Eleven tools over stdio
“MCP-first” is the claim in the first paragraph of the README, and the server is
the part of this repository that most clearly delivers on it. It is 221 lines: two response
helpers, one parser, eleven registerTool calls, and a stdio transport. There is no
state in the server — every tool delegates to a single module-level DaedalusCore.
The whole server, structurally
41export async function startMcpServer(): Promise<void> { 42 const server = new McpServer({ 43 name: "daedalus", 44 version: "0.1.0" 45 }); 46 47 server.registerTool( 48 "daedalus_create_goal_contract", 49 { 50 title: "Create goal contract", 51 description: "Compile a vague user goal into a structured Daedalus goal contract.", 52 inputSchema: { 53 goal: z.string().min(1) 54 } 55 }, 56 async ({ goal }) => textResponse(core.createGoalContract(goal)) 57 ); … 215 const transport = new StdioServerTransport(); 216 await server.connect(transport); 217} 218 219if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) { 220 await startMcpServer(); 221}
The guard at line 219 lets the same module serve as both a library and an executable: the CLI
imports startMcpServer and calls it from the mcp subcommand, while
node dist/packages/mcp-server/src/index.js self-starts. Every tool result goes
through one helper that serialises to a single text content block:
10function textResponse(value: unknown) { 11 return { 12 content: [ 13 { 14 type: "text" as const, 15 text: typeof value === "string" ? value : JSON.stringify(value, null, 2) 16 } 17 ] 18 }; 19}
No structured content, no output schemas, no resource links. A calling agent receives
pretty-printed JSON as text and is expected to read it — or, for
daedalus_propose_next_task, a plain English instruction. That is a deliberate
simplification, and it is the reason the goal contract has to travel back into the server as a
JSON string on three of the eleven tools.
Fig. 5.1 Tool surface — plan view
daedalus_score_attempt is the only tool whose schema is more than two
fields, and the only one that accepts a numeric payload.Tool reference
Descriptions are quoted verbatim from the registerTool metadata.
| Tool name | Title & description | Input | Line |
|---|---|---|---|
| daedalus_create_goal_contract | Create goal contract “Compile a vague user goal into a structured Daedalus goal contract.” |
goal | 47 |
| daedalus_plan_milestones | Plan milestones “Create a milestone DAG for a goal contract.” |
goalContractJson | 59 |
| daedalus_select_evaluators | Select evaluators “Select evaluator packs that can prove a goal contract.” |
goalContractJson | 77 |
| daedalus_create_run | Create run “Create and persist a Daedalus run from a user goal.” |
goal | 95 |
| daedalus_report_status | Report status “Load and return a persisted Daedalus run.” |
runId | 107 |
| daedalus_score_attempt | Score attempt “Record a scored attempt for a run and decide keep/revert/review.” |
runId, attemptId?, milestoneId?, proofScores, failures?, noRegressions? | 119 |
| daedalus_propose_next_task | Propose next task “Return the next milestone task prompt for a builder agent.” |
runId | 146 |
| daedalus_list_adapters | List adapters “List known external tool adapters and their intended role.” |
none | 161 |
| daedalus_freeze_evals | Freeze evals “Snapshot the selected evaluator manifest for a run before builder optimization.” |
runId, notes? | 171 |
| daedalus_open_attempt | Open attempt “Open a new attempt record for a run and optional milestone.” |
runId, milestoneId? | 184 |
| daedalus_propose_missing_evaluators | Propose missing evaluators “Create guarded evaluator proposals for proofs not covered by the registry.” |
goalContractJson | 197 |
The proofScores schema is z.record(z.string(),
z.number().min(0).max(1)) — an open string map, not
z.record(z.enum([...proofs]), …). A caller may submit
{"looks_good": 1} and the server will accept it, cast it to
Partial<Record<Proof, number>> at index.ts:139, and the
scorer will silently ignore it because it iterates the goal’s required proofs rather than the
submitted keys. The Proof union is enforced by the compiler on the way out and
not on the way in.
Notably absent: there is no daedalus_run_evals, no
daedalus_validate_evaluator, and no tool that lists existing runs. The first of
these is item 2 on the outstanding-work list in RESEARCH.md:311 —
“Implement real run_evals execution instead of scoring by submitted proof scores.”
Fig. 5.2 Sequence — one milestone, proposed to accepted
docs/ARCHITECTURE.md:114–119 states the
intended rule — “Artifact-level evidence beats trace-level evidence” — and the protocol as built
cannot yet tell the two apart.Connecting a host
docs/MCP_USAGE.md gives a host configuration for the compiled server. The
cwd matters: without it, or without DAEDALUS_HOME, runs land wherever
the host process started.
$ npm run build # tsc -p tsconfig.json -> dist/
$ npm run dev:mcp # tsx packages/mcp-server/src/index.ts
$ daedalus mcp # same server via the Commander CLI
# or from a built tree:
$ node dist/packages/mcp-server/src/index.js
# MCP host entry, verbatim from docs/MCP_USAGE.md:
{
"mcpServers": {
"daedalus": {
"command": "node",
"args": ["C:/Users/ianig/Desktop/daedalus/dist/packages/mcp-server/src/index.js"],
"cwd": "C:/Users/ianig/Desktop/daedalus"
}
}
}
# when the cwd cannot be set, pin the state root instead:
$ DAEDALUS_HOME=/srv/daedalus node dist/packages/mcp-server/src/index.js
The server writes nothing to stdout other than the MCP protocol stream — there is no logging
call anywhere in packages/mcp-server, which is correct for a stdio transport, where
a stray console.log corrupts the JSON-RPC frame.
The agent prompt is part of the interface
Because the protocol cannot enforce role separation, the repository ships the rule as prose
in two places — docs/MCP_USAGE.md and templates/AGENTS.md — and the
same sentence is appended to every task string returned by
proposeNextTask() at daedalus.ts:160.
“The task is not done because an agent says it is done. It is done when the artifact passes the required proof gates and the score meets the run contract.”
templates/AGENTS.md — CompletionRead against Fig. 5.2, that sentence is aspirational rather than descriptive: the gates are currently passed by the agent asserting numbers. It is nevertheless the correct thing to write down, because the whole point of the control plane is to make the assertion explicit, attached to a named proof, and recorded against a frozen manifest — so that when a real evaluator runner arrives, there is already a ledger for it to fill in.
Sheet notes
- The MCP server imports
DaedalusCorefrom"../../core/src/index.js"— a relative source path, not a workspace package reference. There is no npm workspaces configuration inpackage.json; the monorepo layout is directories plus relative imports. new DaedalusCore()is constructed at module scope (index.ts:8) with the defaultStateStore, so the state root is fixed at import time fromDAEDALUS_HOMEor the process working directory.- The server registers no MCP resources and no prompts. Everything is a tool.
- Tool descriptions say “Select evaluator packs”, but the implementation selects entries
from
evaluatorRegistry, not from the JSON files underevaluator-packs/. The two vocabularies have drifted.