Browse the blog

We Gave a Coding Agent Our FastMCP Migration Prompt. Here's What It Did

  • fastmcp
  • migration
  • coding-agents
  • frontmcp
  • mcp

Dated article; use the maintained reference for current instructions. Example version: FrontMCP 1.10.11. Technical checks recorded: 2026-10-26. How checks work.

Migrating from FastMCP has a section called "Migrate with a coding agent". It's a prompt: paste it into your agent, and it moves a FastMCP for TypeScript server to FrontMCP one part at a time, with tests that check clients see the same answers. We wrote the prompt, so we wanted to know what an agent actually does with it.

We gave it to a coding agent, with FastMCP's own example server and nothing else, answered its questions as the developer would, and then checked the result with a client of our own. This post is that run: what the agent did at each stop, what a client sees now, what it got wrong, and the two mistakes it found in our guide. The prompt is below, ready to copy into your own agent.

The result

Agent timeAbout 21 minutes, 109 tool calls
Our replies2: "The list is right. Go ahead" and "Keep public"
Tests the agent wrote51, all passing, in 6 files
FrontMCP code256 lines in 12 files, next to the FastMCP code, which still runs
Client requests with the same answer11 of 18, checked by us with the official MCP SDK client
Differences it didn't listNone
Claims it got wrong1, about an error message's prefix
Fixes to our guide2: the FastMCP health check's text, and differences the guide didn't list

The prompt

This is the whole prompt, as the guide has it. Paste it into your coding agent from the root of a project that uses the fastmcp package. It names the current FrontMCP release; the run below used it with 1.10.11.

Prompt for your coding agent
Migrate this project's FastMCP for TypeScript server (the `fastmcp` npm package) to FrontMCP 1.10.11, without changing what its MCP clients see: tool names, input schemas, result text, error messages, resource URIs and prompt messages.

Follow the guide at https://frontmcp.dev/learn/migrating-from-fastmcp, one section at a time. If FrontMCP's docs MCP server (https://mcp.frontmcp.dev) is connected, read the guide with its get_page tool and look up every FrontMCP API you use with lookup_api and get_example, passing version "1.10.11". Otherwise fetch https://frontmcp.dev/learn/migrating-from-fastmcp.md and the pages it links to. Don't write FrontMCP code from memory.

1. Check the library first. If the project uses `@prefecthq/fastmcp-ts` instead of `fastmcp`, stop and tell me: that is a different library, and this guide doesn't cover it.
2. List what the server has: every addTool, addResource, addResourceTemplate and addPrompt; authenticate; the transport, its endpoint and port; what tools use from their context (log, reportProgress, elicit, session, sessionId); UserError; and any state kept per session. Show me the list before you change code.
3. Record what clients see now, as the guide's "Record what clients see now" section does: drive the FastMCP server in-process with the official MCP SDK's in-memory client, and save its answers, including a call with invalid input.
4. Add FrontMCP next to FastMCP as https://frontmcp.dev/learn/installation#adding-frontmcp-to-an-existing-project shows, with @frontmcp/sdk and frontmcp pinned to 1.10.11. Keep the FastMCP server working until I switch clients.
5. Port one part at a time, in the guide's order: tools; errors, logs and progress; resources, templates and prompts; state kept per session; auth; transports. After each part, write @frontmcp/testing tests that check the recorded answers, run them, and don't start the next part until they pass.
6. Where FrontMCP answers differently by design, keep FrontMCP's behaviour and list the difference for me instead of working around it: invalid input is an INVALID_INPUT tool result, not a -32602 error; input schemas name JSON Schema draft 2020-12 without additionalProperties; log messages are opt-in. A tool that returned a string returns { content: [{ type: "text", text }] } to keep its text. State keyed by the session starts over for MCP 2026-07-28 clients: key it by a handle or the signed-in user, as the guide shows.
7. Ask me before you rename anything a client sees, choose an auth mode, or delete FastMCP code.

When you finish, report what you ported, the test results, every difference a client will see, anything with no FrontMCP counterpart, and what to change in each client before switching it: the URL path, the stdio command, the health check and the address the server listens on.

The agent does the rest in that order: it checks the library, lists what the server has and waits for you, records what clients see, ports one part at a time with tests, and asks before it renames anything, picks an auth mode or deletes FastMCP code.

The setup

The server. FastMCP 4.23.4's own example, src/examples/addition.ts, in a project of its own. It's a good test because it uses most of FastMCP: three tools with Zod, ArkType and Valibot schemas, a two-second timeoutMs, log, reportProgress, the non-standard streamContent, a resource, a resource template, a prompt and server.embedded(). We changed two things so it would run as a user's project: its import now says "fastmcp", and we added @valibot/to-json-schema, without which FastMCP's tools/list fails with Missing dependencies "@valibot/to-json-schema".

The agent. Claude Opus 5.5 in Claude Code, as a new agent with no context but the project's path and the prompt, word for word, for FrontMCP 1.10.11. We added one instruction for the harness: where the prompt says to show or ask the developer, stop and wait for a reply. FrontMCP's docs MCP server wasn't connected, so the agent took the prompt's other route and read the guide's Markdown pages.

What we checked. Before the run, we drove the FastMCP server over stdio with the official MCP SDK client, 1.32.2, and recorded 18 requests: tools/list, every tool, invalid input, the timeout, an unknown tool, every resource and template read, and the prompt with and without its argument. After the run, we sent the same 18 requests to the FrontMCP server. The script lived outside the project, where the agent couldn't see it.

Stop 1: what the server has

After 93 seconds the agent stopped, as the prompt asks, with a list of everything the server has: the six tools with their schema libraries and annotations, which context functions each one uses, the resource, the template, the prompt, the two transports, and what isn't there (no authenticate, no state kept per session). It noted that the strings in v.number("The first number") are Valibot error messages, not descriptions, which the recorded schema confirmed.

It also found two things we hadn't asked about, and both were right:

  • console.log writes to stdout. Over stdio that's the protocol channel, and the server sends Started stdio transport with ping disabled by default and [Zod] Adding 2 and 3 there, between its JSON-RPC messages.
  • FastMCP's health check says ✓ Ok. FastMCP's README says the default body is ok, and our guide repeated it. The installed code sends healthConfig.message ?? "✓ Ok". The agent read the code, not the README.

Stop 2: four parts, with tests

The prompt asks for a recording first, made with the official SDK's in-memory client. The guide's recording script imports a server that hasn't started, so the agent moved FastMCP's registrations, unchanged, into a module that doesn't call start(), and kept every start option in src/server.ts. It checked that a fresh recording of FastMCP came out byte for byte the same afterwards.

Then it added FrontMCP 1.10.11 next to FastMCP and ported the server one part at a time. Each ArkType and Valibot schema became a Zod schema with the same properties. This is add-arktype in FastMCP, without the example's comments and console.log:

server.addTool({
  name: "add-arktype",
  description: "Add two numbers (using ArkType schema)",
  parameters: type({ a: "number", b: "number" }),
  annotations: { title: "Addition (ArkType)", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true },
  timeoutMs: 2000,
  execute: async (args, { log }) => {
    log.info("Starting calculation with potential delay...");
    if (args.a > 1000 || args.b > 1000) {
      log.warn("Large numbers detected, operation might take longer");
      await new Promise((resolve) => setTimeout(resolve, 3000));
    }
    return String(args.a + args.b);
  },
});

And this is the agent's FrontMCP version, as it wrote it:

@Tool({
  name: "add-arktype",
  description: "Add two numbers (using ArkType schema)",
  inputSchema: addParameters.shape,
  annotations: {
    title: "Addition (ArkType)",
    readOnlyHint: false,
    destructiveHint: true,
    idempotentHint: true,
    openWorldHint: true,
  },
  timeout: { executeMs: 2000 },
})
export class AddArkType extends ToolContext {
  async execute({ a, b }: z.infer<typeof addParameters>) {
    this.logger.info(`[ArkType] Adding ${a} and ${b}`);
    await this.notify({ message: "Starting calculation with potential delay..." }, "info");
    if (a > 1000 || b > 1000) {
      await this.notify({ message: "Large numbers detected, operation might take longer" }, "warning");
      await new Promise((resolve) => setTimeout(resolve, 3000));
    }
    return { content: [{ type: "text", text: String(a + b) }] };
  }
}

timeoutMs became timeout: { executeMs }, log became this.notify() with the same data, the console.log moved to the server's logger, off stdout, and the string result became a text block, so its text stays "5". server.embedded() has no counterpart, so the documents moved into a module that the template and get-documentation both import, and the tool's resource block came out the same.

Its tests read the recording and compare. This one checks every tool's tools/list entry against FastMCP's, with the two schema keywords FrontMCP changes by design:

for (const toolName of recordedToolNames) {
  test(`${toolName} is listed as recorded, with a draft 2020-12 schema and no additionalProperties`, async ({ mcp }) => {
    const recorded = recordedTool(toolName);
    const listed = (await mcp.tools.list()).find((tool) => tool.name === toolName);
    expect(listed).toEqual({ ...recorded, inputSchema: inputSchemaAsFrontMcpSendsIt(recorded.inputSchema) });
  });
}

After four parts and 40 passing tests, it stopped at auth, which the prompt says to ask about. FastMCP had no authenticate, so it recommended FrontMCP's default, public, and listed what clients would notice anyway: a bearer token FrontMCP didn't sign gets 401, and the server answers two new paths, its protected-resource metadata and a token endpoint, /oauth/token. We said to keep public.

Stop 3: transports, and a report

In five more minutes it ported auth and transports, with 11 more tests, and wrote its report. The report lists 18 differences a client will see, the FastMCP features with no counterpart (streamContent, server pings, the automatic roots request, /ready), and what to change in each client before switching it: the stdio command becomes FRONTMCP_STDIO=1 with src/main.ts, /health answers JSON, readiness moves to /readyz, and FrontMCP listens on 127.0.0.1 and checks the Host header.

Then it stopped once more, to ask before deleting the FastMCP code, as the prompt says. We left it there: FastMCP keeps running until the clients have switched.

What a client sees now

Our own client found 11 of the 18 answers identical, byte for byte. Every one of the other 7 is on the agent's list, and each is FrontMCP behaving as designed rather than a porting mistake:

  • tools/list names JSON Schema draft 2020-12 without additionalProperties, and lists the tools by name rather than in the order they were added.
  • Invalid input and unknown tools come back as tool results with isError and a code, INVALID_INPUT or TOOL_NOT_FOUND, instead of JSON-RPC -32602 errors.
  • The timeout and the prompt's missing argument have new message text.
  • Log messages are sent only after the client asks for them.
  • stream-poem returns the same text, without FastMCP's four notifications/tool/streamContent messages.

The run also made the server newer: FastMCP 4.23.4 answers MCP 2026-07-28's server/discover with -32601 Method not found, and FrontMCP answers it with the five revisions it serves, 2026-07-28 first.

What it got wrong, or decided by itself

  • One wrong claim. The report says FastMCP's error messages start with MCP error -32602: on the wire and FrontMCP's don't. On the wire both do. FrontMCP also sends data.code, MISSING_PROMPT_ARGUMENT for the prompt.
  • The poem lost its pauses. FastMCP's stream-poem waits a second between lines while it streams them. With nothing to stream, the agent dropped the waits, so the tool answers at once instead of after four seconds. It listed this as a difference; it didn't ask.
  • The tsconfig changed. frontmcp init, which the installation page says to run, rewrote tsconfig.json. The agent reported it and checked that FastMCP still typechecks.
  • A detail worth more weight. Without NODE_ENV=production, FrontMCP's INVALID_INPUT and TOOL_NOT_FOUND results carry the stack in _meta.stack, with the server's file paths; with it, they don't. The agent mentioned the stack trace in one line of the report. We'd put it at the top.

We re-ran its 51 tests and the typecheck ourselves: both pass, the tests in 18 seconds.

What we changed in the guide

The run found things the guide had wrong or didn't say, and we checked each one before changing it:

  • The health check's text: FastMCP 4.23.4 answers /health with ✓ Ok, not ok.
  • Two rows in "What maps to what": timeoutMs and the timeout's new text, and server.embedded(), with the shared module the agent used.
  • Two differences in tools: an unknown tool's TOOL_NOT_FOUND result, and tools/list sorted by name.
  • Three things to check before switching clients: readiness on /readyz, CORS, which FastMCP allowed from any origin and FrontMCP doesn't send until http.cors is set, and running with NODE_ENV=production.

Run it on your server

Copy the prompt above, or from Migrate with a coding agent in the guide. If your agent is connected to the docs MCP server, it's also the server's migrate_from_fastmcp prompt, with a version argument; with Claude Code:

claude mcp add --transport http frontmcp-docs https://mcp.frontmcp.dev

Two things from this run are worth doing yourself: record what your clients see before the agent starts, with a client of your own rather than the agent's tests, and read the report's differences before you answer its question about deleting FastMCP.

What one run doesn't show

This is one run, with one model and one example server, on October 26, 2026. The server has no auth and no state per session, which are the parts of the guide most likely to need a person's judgment. We didn't switch a real client, and we didn't record token use or cost. A run with another agent or on your own server can come out differently; if it does, open an issue with what happened.

Report a docs issue. Site usage and privacy.