CLI tool
punkpeye/fastmcp avatar
punkpeye/fastmcp

FastMCP: legacy protocol revision, a 4.22 tag train, and a 1.0.0 manifest

A TypeScript framework for building MCP servers.

3,288 stars340 forksTypeScriptMIT

At a glance

What is it?
punkpeye/fastmcp is a TypeScript framework for MCP servers that says up front it implements a superseded protocol revision, ships three releases eight minutes apart in the 4.22 line while its own manifest still reports 1.0.0, and points its documentation at a single frozen commit rather than at any tag.
Who is it for?
Use it if your clients are on the handshake based revisions and you want an opinionated layer over the official SDK, and pin by git tag rather than by the manifest, which still says 1.0.0. Decide the protocol question before anything else: the README states plainly that the 2026-07-28 stateless revision is unsupported and names a different framework for it, so building on this one against current clients means planning around a callout the maintainers wrote themselves.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 13, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • punkpeye/fastmcp3,288 ★ · +24
Star history
Repository2026-09-112026-11-13
punkpeye/fastmcp3.3k3.3k

24 Stars in the last 63 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

The README opens by declaring that the protocol revision it implements is superseded

The first thing the document says after the title is not a feature. It is an IMPORTANT callout stating that FastMCP implements the legacy, handshake-based MCP revisions of 2025-11-25 and earlier, that it does not support the current specification dated 2026-07-28, and that the current specification made the protocol stateless, with no initialize handshake and no Mcp-Session-Id. It then names a different framework for anyone targeting the current spec.

Read the feature list against that and several entries stop being generic capabilities and become consequences of the older revision. Sessions is a feature. Session ID and Request ID tracking is a feature. Configurable ping behaviour is a feature. All three exist because the protocol used to have a handshake and a session header, and the reason the framework still carries them is the reason it can no longer talk to a current client without negotiation.

Stateless mode appears in the same list, positioned for serverless deployments, which is the one entry that acknowledges the direction the protocol took. It is a mode a server can be put into, not a spec the server speaks, so it does not undo the callout.

That callout is more useful than it first looks. A framework that quietly kept implementing an old revision would be a trap, and one that loudly says so is telling you to make an explicit choice. The cost of the choice is the thing to weigh: if your clients are current, this framework's session layer is decoration, and the README has already told you where to go instead.

Worth noting alongside it: the repository's declared homepage is glama.ai/mcp/servers, a directory listing rather than a documentation site, and the package author contact is an address at the same domain.

Three tags published eight minutes apart while the package still says version 1.0.0

The three most recent releases were all published on 2026-10-04 within a single morning. v4.22.2 at 02:31:16, v4.22.3 at 02:33:30, v4.22.4 at 02:39:03. That is a patch train moving at four-minute and six-minute intervals, which is the signature of an automated publish rather than three deliberate releases.

The last commit to the default branch is stamped 2026-10-04T02:38:57Z, six seconds before v4.22.4 was published. So the newest tag follows the newest commit almost immediately, which means a build of the tip of main and the newest release are close to the same thing today and may not be tomorrow.

Now put that next to package.json, where the version field reads 1.0.0. The in-tree metadata does not track the release line at all. There is no version field of 4.22.4 anywhere in the manifest, so anything that reads the installed package to learn what it is, rather than resolving a tag, gets 1.0.0. The quickstart compounds the ambiguity by constructing its server with version: "1.0.0" as well, so a reader who copies the example and then asks the framework to report its own version gets the same wrong answer twice.

The practical rule that falls out of this: treat the git tag as the version of record and the manifest as decorative. That is workable, but it is the opposite of the usual convention, and it is worth knowing before you file a bug that includes a version number.

Every reference link points at one frozen commit, and one line range serves two features

The section comparing FastMCP to the official SDK is where the project explains its own value, and it does it by linking to its own source. Each bullet links to a line range inside src/FastMCP.ts, and every one of those links is a permalink to a single commit, 06c2af7a3d7e3d8c638deac1964ce269ce8e518b.

That is deliberate and it is the right call for line numbers, since ranges drift. But it also means the code the documentation points at is frozen at one commit while the release tags are in the 4.22 line. A reader sent to L1303-L1498 to see how tools are handled is reading code from an unspecified point in the 4.x series, not from the version they installed, and there is no way to tell which from the page.

Two details in the ranges are worth a second look. The range L760-L850 is attached to two different bullets, handling of connections and adding prompts, so either one link covers both concerns or one of them is pointing at the wrong place. And the bullet for adding resources points at L960-L962, a three line range, while the bullet immediately after it, resource templates, points at L964-L987, twenty four lines. A three line range is not much of an implementation to be sent to.

The ranges for the embedded content blocks are more generous: L1569-L1643 for resources, L51-L111 for image, L113-L173 for audio. So the one place a reader is sent to a handful of lines rather than a couple of hundred is the one covering resources themselves.

Starting httpStream also starts an SSE server you did not ask for

The remote server options section is where the transport behaviour is actually specified, and one line in it deserves more attention than it gets. Starting a server with HTTP streaming support looks like this:

ts
server.start({
  transportType: "httpStream",
  httpStream: {
    port: 8080,
  },
});

That listens for HTTP streaming connections on http://localhost:8080/mcp. The endpoint path is configurable through the httpStream.endpoint option, which defaults to /mcp. There is also an httpStream.basePath option, for serving HTTP streaming and the built-in OAuth routes under an issuer path, with the example given as /issuer1, which exposes authorization server metadata at /.well-known/oauth-authorization-server/issuer1 per RFC 8414.

Then the note that matters: this also starts an SSE server on http://localhost:8080/sse.

So a single start call binds two endpoints on the same port, and the second one is not opt in. Nothing in that section mentions a switch to turn it off, and CORS is listed as enabled by default in the feature list. Authentication is a separate feature with its own section, and the transport section only brings OAuth up in passing, in the context of an issuer path for metadata discovery.

For a deployment that is fine, and the SSE endpoint is a courtesy to older clients. For a deployment where you bound the port believing you were starting one thing, it is worth knowing that a second listener arrives with it before you choose a port you intend to expose.

Four export entry points ship dual format while the README names none of them

The manifest defines four public entry points. The root maps to dist/FastMCP.js for imports, dist/FastMCP.d.ts for types and dist/FastMCP.cjs for require. Then there is ./auth mapping to dist/auth/index.js, .d.ts and .cjs. Then ./edge mapping to dist/edge/index.js, .d.ts and .cjs. Then ./openapi mapping to dist/openapi/index.js, .d.ts and .cjs.

That is twelve build artifacts across four entry points, because every entry ships both an ES module and a CommonJS file. The package declares type module and is built with tsup, so both formats are produced deliberately rather than by accident, which is a real convenience for consumers stuck on require.

The gap is on the documentation side. Authentication, edge runtime support and OpenAPI to MCP conversion are all named as headline features in the feature list, with their own anchor links. None of the three section links ever tells you that the code lives behind a subpath export, and a reader who greps the README for the string ./auth or ./openapi finds nothing. The only subpath mentioned anywhere in the visible documentation is require("none-ls..." in a different project entirely.

So the feature list describes capabilities and the manifest describes where to import them, and the join between the two is left to the reader. It is a small thing, and it is the kind of small thing that costs someone an afternoon.

The install path and the example smoke test use different package managers

There are two ways into this project and they do not agree on tooling. For using it, the Installation section is one line:

bash
npm install fastmcp

For testing it, the Quickstart section gives a different sequence: clone the repository, change into it, run pnpm install, run pnpm build, then run npx fastmcp dev against src/examples/addition.ts to test the addition server example with the CLI, or npx fastmcp inspect against the same file to test it with MCP Inspector.

So consumers install from npm with npm, while the project's own verification path clones the tree and installs with pnpm. The repository carries pnpm-lock.yaml rather than a package-lock.json, which makes pnpm the intended developer tool, and the presence of both a package-lock equivalent and the pnpm lockfile in different workflows is exactly the kind of thing that produces subtly different local trees.

The more important detail is what the smoke test actually exercises. It runs src/examples/addition.ts, a file in the working tree, through a binary that the manifest maps to dist/bin/fastmcp.js. So the documented way to check that the project works is to build it and run its own source, not to install the published tarball and run that. A packaging regression that only manifests in the published artifact, a missing file in the dist output, an export map entry that does not resolve, would survive the documented check.

It also means the example the quickstart tells you to copy is verified only by the branch you are standing on.

The OpenAPI network tests are opt-in behind an environment variable

The scripts section of the manifest has five entries and one of them tells you more about the project's testing posture than the other four combined. test is vitest run. test:openapi sets OPENAPI_NETWORK to 1 and then runs vitest against two named files, src/openapi/fromOpenAPI.benchmark.test.ts and src/openapi/fromOpenAPI.live.test.ts. test:openapi:refresh runs a script at scripts/refresh-openapi-benchmark-specs.mjs. build is tsup. format writes with prettier and fixes with eslint.

The gate is the environment variable. A plain vitest run does not set OPENAPI_NETWORK, so the two OpenAPI files are excluded from the default test run. Those are the tests that talk to the network, and they are named after what they do: one is a benchmark, one is live.

That is a defensible choice, because tests that hit the network fail for reasons that have nothing to do with the code. But it does mean the OpenAPI conversion feature, which is a headline item in the feature list, is the part of the codebase with the least automatic coverage on a default run. The separate refresh script also implies those benchmark specifications are stored fixtures that can go stale against a live API, and refreshing them is a manual step rather than part of CI.

The lint script is the other one worth reading closely, because it chains four tools into a single command: prettier --check, eslint, tsc --noEmit, and then jsr publish --dry-run. A dry run against the JavaScript Registry is inside the lint script, which means the lint step doubles as a packaging validation for a second registry.

A framework for MCP servers depends on an MCP proxy and publishes two keywords

The dependency list is twelve packages and two of them are worth pausing on. One is @modelcontextprotocol/sdk at ^1.24.3, the official SDK the README says FastMCP is built on top of. The other is mcp-proxy at ^6.7.19. A framework that already wraps the SDK also embeds a proxy for the same protocol, which is a reasonable tool to have available for bridging servers but is not mentioned anywhere in the feature list.

The rest of the list maps cleanly onto the feature list, which is a sign the feature list is honest. @apidevtools/swagger-parser and xsschema are OpenAPI to JSON Schema to MCP conversion. execa is spawning command line tools, file-type is sniffing MIME types for the image and audio content blocks, uri-templates is the resource template expansion, hono is the HTTP layer underneath the custom routes and the health check endpoint, yargs is the CLI behind the dev and inspect subcommands, and fuse.js is a fuzzy search library, which for a framework whose job is not search is the one entry whose purpose is not obvious from the feature list.

Then there are the keywords, and there are two of them: MCP and SSE. For a package whose distinguishing features include OpenAPI conversion, authentication, edge runtime support, audio and image content and stateless serverless mode, two keywords is thin. One of the two also names the transport the project now positions as the fallback rather than the recommendation, since the remote server section leads with HTTP streaming and mentions SSE as something that starts alongside it.

The repository has 3272 stars, 321 forks and one open issue, which is a very quiet issue tracker for that much adoption. Whatever the cause, the documentation load is being carried almost entirely by one README.

Editorial conclusion

Use it if your clients are on the handshake based revisions and you want an opinionated layer over the official SDK, and pin by git tag rather than by the manifest, which still says 1.0.0. Decide the protocol question before anything else: the README states plainly that the 2026-07-28 stateless revision is unsupported and names a different framework for it, so building on this one against current clients means planning around a callout the maintainers wrote themselves. Also check what your start call binds, since one httpStream configuration opens an SSE endpoint alongside the streaming one, and read the export map rather than the feature list to find the auth, edge and openapi subpaths.

Frequently asked questions

how to install fastmcp

The Installation section is one line, npm install fastmcp. It is a TypeScript package built with tsup and published to npm. The repository also carries jsr.json and its lint script runs a jsr publish dry run, so a second registry exists without being named in the install instructions.

What is the current version of FastMCP?

The tags say v4.22.4, published 2026-10-04T02:39:03Z, following v4.22.3 and v4.22.2 earlier the same morning. The version field in package.json is 1.0.0, and the quickstart constructs its server with version 1.0.0 as well, so the in-tree metadata does not track the release line.

Does FastMCP implement the current MCP specification?

No, and the README says so in an IMPORTANT callout at the top. It implements the legacy handshake-based revisions of 2025-11-25 and earlier, and does not support 2026-07-28, which made the protocol stateless with no initialize handshake and no Mcp-Session-Id. For a framework targeting the current spec it points at ViteMCP.

What does starting an httpStream FastMCP server actually bind?

Two endpoints on the same port. The streaming server listens at /mcp by default, configurable through httpStream.endpoint, and the README notes this also starts an SSE server at /sse. Setting httpStream.basePath, for example /issuer1, moves both under an issuer path and exposes OAuth authorization server metadata at /.well-known/oauth-authorization-server/issuer1 per RFC 8414.

Can I unit test a FastMCP server without binding a port?

Yes. The feature list names an in-memory transport for unit testing without binding a port. There is also a CLI with a dev subcommand for testing and an inspect subcommand for the MCP Inspector, both documented against the addition example at src/examples/addition.ts.

What subpath exports does the fastmcp package provide?

Four: the root entry plus ./auth, ./edge and ./openapi. Each is published with an import target, a types declaration file and a require target pointing at a .cjs build, while the package itself declares type module. None of the three subpaths is named in the feature list.

Official sources

  1. License: MIT
  2. Project website
  3. punkpeye/fastmcp on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/punkpeye-fastmcp.svg)](https://hysenlabs.com/projects/punkpeye-fastmcp)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/punkpeye-fastmcp.svg)](https://hysenlabs.com/projects/punkpeye-fastmcp)

Was this useful?