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

> We gave a coding agent our guide's prompt and FastMCP's own example server. What it ported in 21 minutes, what clients see now, and what it got wrong.

Source: https://frontmcp.dev/blog/coding-agent-fastmcp-migration
Published: 2026-10-26
Author: David Antoon
Example version: FrontMCP 1.10.11
Technical checks recorded: 2026-10-26
Scope: Dated article; use the maintained reference for current instructions. Its presence in a release snapshot does not verify its code against that release.

*[Illustration: FastMCP 4.23.4's own example server, src/server.ts with six addTool calls, a resource, a template and a prompt, goes to a coding agent with the guide's prompt. The agent stops three times: to list what the server has before changing code, to ask about auth, and to ask before deleting FastMCP. It ends with FrontMCP 1.10.11 next to FastMCP: six tool classes, two resources, a prompt, six test files and the recorded FastMCP answers, with 51 of 51 tests passing, after about 21 minutes and two replies from us.]*

[Migrating from FastMCP](https://frontmcp.dev/learn/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](https://frontmcp.dev/blog/coding-agent-fastmcp-migration#the-prompt) is below, ready to copy into your own agent.

<a id="the-result"></a>

## The result

| | |
| --- | --- |
| Agent time | About 21 minutes, 109 tool calls |
| Our replies | 2: "The list is right. Go ahead" and "Keep public" |
| Tests the agent wrote | 51, all passing, in 6 files |
| FrontMCP code | 256 lines in 12 files, next to the FastMCP code, which still runs |
| Client requests with the same answer | 11 of 18, checked by us with the official MCP SDK client |
| Differences it didn't list | None |
| Claims it got wrong | 1, about an error message's prefix |
| Fixes to our guide | 2: the FastMCP health check's text, and differences the guide didn't list |

<a id="the-prompt"></a>

## 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.

```text title="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.

<a id="the-setup"></a>

## The setup

**The server.** FastMCP 4.23.4's own example, [`src/examples/addition.ts`](https://github.com/punkpeye/fastmcp/blob/v4.23.4/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](https://frontmcp.dev/blog/coding-agent-fastmcp-migration#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](https://frontmcp.dev/learn/docs-for-your-agent) 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.

*[Illustration: The run on a time axis of about 21 minutes and 109 tool calls. After 1.5 minutes, the agent lists what the server has and asks to go ahead; we answer that the list is right. Until minute 16 it records FastMCP's answers, adds FrontMCP and ports four parts, tools, errors, logs and progress, resources and prompts, and state, with 40 tests, then asks whether to keep auth public; we answer keep public. By minute 21 it has ported auth and transports, with 11 more tests, writes the report, and asks whether it can delete FastMCP.]*

<a id="stop-1-what-the-server-has"></a>

## 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.

<a id="stop-2-four-parts-with-tests"></a>

## 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`:

```ts
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:

```ts
@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:

```ts
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`.

<a id="stop-3-transports-and-a-report"></a>

## 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.

<a id="what-a-client-sees-now"></a>

## What a client sees now

*[Illustration: The same 18 requests from the official MCP SDK client over stdio, before and after. Tools: tools/list differs, draft 2020-12 schemas sorted by name; add-zod is the same; add-zod with invalid input differs, an INVALID_INPUT result; add-arktype has the same text with the log message now opt-in; the add-arktype timeout has new message text; add-valibot is the same; stream-poem has the same text with no stream; test-buffering-fix is the same, progress too; get-documentation is the same. Errors, resources and prompts: an unknown tool differs, a TOOL_NOT_FOUND result; resources/list, reading file:///logs/app.log, resources/templates/list, reading docs://project/deployment, reading an unknown section, prompts/list and getting git-commit are the same; getting git-commit without its argument has new message text.]*

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.

<a id="what-it-got-wrong-or-decided-by-itself"></a>

## 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.

<a id="what-we-changed-in-the-guide"></a>

## 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`.

<a id="run-it-on-your-server"></a>

## Run it on your server

Copy [the prompt](https://frontmcp.dev/blog/coding-agent-fastmcp-migration#the-prompt) above, or from [Migrate with a coding agent](https://frontmcp.dev/learn/migrating-from-fastmcp#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:

```bash
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.

<a id="what-one-run-doesnt-show"></a>

## 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](https://github.com/agentfront/frontmcp/issues) with what happened.
