Implement MCP 2026-07-28 (#2286)

Co-authored-by: Matt <77928207+mattzcarey@users.noreply.github.com>
Co-authored-by: Konstantin Konstantinov <KKonstantinov@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Felix Weinberger 2026-06-26 14:01:10 +01:00 • committed by GitHub
commit 1823aae891
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 69131 additions and 9001 deletions

View file

@ -0,0 +1,6 @@
# bearer-auth
Resource-server-only auth: `requireBearerAuth` from `@modelcontextprotocol/express` in front of `createMcpHandler`. The client asserts `401` + `WWW-Authenticate` without a token, and that the verified `authInfo` reaches the factory (`ctx.authInfo`) with
one.
**HTTP-only** by definition. The full interactive OAuth set lives under `../oauth/` (run headlessly in CI via the demo AS's auto-consent mode).

View file

@ -0,0 +1,31 @@
/**
* Asserts a bare request is `401` with a `WWW-Authenticate` header, and that
* a request with `Authorization: Bearer demo-token` reaches the `whoami` tool
* with the verified `authInfo`.
*/
import { check, parseExampleArgs } from '@mcp-examples/shared';
import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client';
const { url, era } = parseExampleArgs();
// Unauthenticated → 401 + WWW-Authenticate.
const unauth = await fetch(url, {
method: 'POST',
headers: { 'content-type': 'application/json', accept: 'application/json, text/event-stream' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'ping' })
});
check.equal(unauth.status, 401);
check.match(unauth.headers.get('www-authenticate') ?? '', /Bearer/);
// Authenticated → 200 and the tool sees the authInfo. Bearer auth is
// HTTP-layer and era-agnostic; the client honours `--legacy` via `era`.
const client = new Client(
{ name: 'bearer-auth-example-client', version: '1.0.0' },
{ versionNegotiation: { mode: era === 'modern' ? 'auto' : 'legacy' } }
);
await client.connect(new StreamableHTTPClientTransport(new URL(url), { authProvider: { token: async () => 'demo-token' } }));
const result = await client.callTool({ name: 'whoami', arguments: {} });
check.equal(result.content?.[0]?.type === 'text' ? result.content[0].text : '', 'client=demo-client');
await client.close();

View file

@ -0,0 +1,28 @@
{
"name": "@mcp-examples/bearer-auth",
"private": true,
"type": "module",
"scripts": {
"server": "tsx server.ts",
"client": "tsx client.ts"
},
"dependencies": {
"@mcp-examples/shared": "workspace:*",
"@modelcontextprotocol/client": "workspace:*",
"@modelcontextprotocol/express": "workspace:*",
"@modelcontextprotocol/node": "workspace:*",
"@modelcontextprotocol/server": "workspace:*",
"zod": "catalog:runtimeShared"
},
"devDependencies": {
"tsx": "catalog:devTools"
},
"example": {
"transports": [
"http"
],
"era": "dual",
"path": "/mcp",
"//": "Bearer auth + 401/WWW-Authenticate is HTTP-layer and era-agnostic; the client honours --legacy via the inline versionNegotiation branch."
}
}

View file

@ -0,0 +1,51 @@
/**
* Minimal Resource-Server-only auth: `requireBearerAuth` + `OAuthTokenVerifier`
* in front of `createMcpHandler`. The verifier accepts a single static
* `demo-token`; the verified `authInfo` reaches the factory as `ctx.authInfo`.
*
* No Authorization Server here, and no metadata endpoints — see `examples/oauth/`
* for the full RS + AS discovery flow. HTTP-only by definition.
*/
import { parseExampleArgs } from '@mcp-examples/shared';
import type { OAuthTokenVerifier } from '@modelcontextprotocol/express';
import { createMcpExpressApp, requireBearerAuth } from '@modelcontextprotocol/express';
import { toNodeHandler } from '@modelcontextprotocol/node';
import type { AuthInfo, McpServerFactory } from '@modelcontextprotocol/server';
import { createMcpHandler, McpServer, OAuthError, OAuthErrorCode } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';
const buildServer: McpServerFactory = ctx => {
const server = new McpServer({ name: 'bearer-auth-example', version: '1.0.0' });
server.registerTool('whoami', { description: 'Returns the authenticated subject.', inputSchema: z.object({}) }, async () => ({
content: [{ type: 'text', text: `client=${ctx.authInfo?.clientId ?? 'anon'}` }]
}));
return server;
};
const { port } = parseExampleArgs();
// Replace with JWT verification, RFC 7662 introspection, etc.
const staticTokenVerifier: OAuthTokenVerifier = {
async verifyAccessToken(token): Promise<AuthInfo> {
if (token !== 'demo-token') {
throw new OAuthError(OAuthErrorCode.InvalidToken, 'unknown token');
}
return { token, clientId: 'demo-client', scopes: ['mcp'], expiresAt: Math.floor(Date.now() / 1000) + 3600 };
}
};
// Bearer auth is HTTP-layer (no stdio arm). The MCP handler is the canonical
// `createMcpHandler(buildServer)`; the Express auth middleware in front of it
// is the point of this story.
const handler = createMcpHandler(buildServer);
const app = createMcpExpressApp();
const auth = requireBearerAuth({ verifier: staticTokenVerifier, requiredScopes: ['mcp'] });
// `requireBearerAuth` sets `req.auth`; `toNodeHandler` reads it and passes it
// to the factory as `ctx.authInfo`.
const node = toNodeHandler(handler);
app.all('/mcp', auth, (req, res) => void node(req, res, req.body));
app.listen(port, () => {
console.error(`[server] listening on http://127.0.0.1:${port}/mcp`);
});