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:
parent
e4227d135c
commit
1823aae891
1 changed files with 69131 additions and 9001 deletions
77
examples/oauth-client-credentials/server.ts
Normal file
77
examples/oauth-client-credentials/server.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
/**
|
||||
* OAuth 2.0 **`client_credentials`** grant — the machine-to-machine story.
|
||||
*
|
||||
* One process hosts both halves on adjacent ports:
|
||||
*
|
||||
* - `:PORT` — the MCP **Resource Server**: `createMcpHandler` behind
|
||||
* `requireBearerAuth`, advertising the AS via `mcpAuthMetadataRouter`
|
||||
* (RFC 9728 Protected Resource Metadata + RFC 8414 AS metadata).
|
||||
* - `:PORT+1` — a minimal in-repo **Authorization Server** that supports the
|
||||
* `client_credentials` grant only (`@mcp-examples/shared`'s
|
||||
* `createClientCredentialsAuthServer`). The full better-auth/OIDC demo AS
|
||||
* only implements `authorization_code`, hence this purpose-built one.
|
||||
*
|
||||
* The client (`./client.ts`) discovers the AS from a 401 challenge, exchanges
|
||||
* its `client_id`/`client_secret` for a Bearer token at `/token`, and reaches
|
||||
* the `whoami` tool — which echoes `ctx.authInfo` so the client can assert the
|
||||
* granted scopes round-tripped end to end. HTTP-only by definition.
|
||||
*/
|
||||
import { parseExampleArgs } from '@mcp-examples/shared';
|
||||
import { createClientCredentialsAuthServer } from '@mcp-examples/shared/auth';
|
||||
import {
|
||||
createMcpExpressApp,
|
||||
getOAuthProtectedResourceMetadataUrl,
|
||||
mcpAuthMetadataRouter,
|
||||
requireBearerAuth
|
||||
} from '@modelcontextprotocol/express';
|
||||
import { toNodeHandler } from '@modelcontextprotocol/node';
|
||||
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
|
||||
import * as z from 'zod/v4';
|
||||
|
||||
const { port } = parseExampleArgs();
|
||||
const AUTH_PORT = port + 1;
|
||||
// 127.0.0.1 (not `localhost`) so the PRM `resource` value matches the URL the
|
||||
// runner passes the client byte-for-byte — the SDK auth driver enforces that.
|
||||
const mcpServerUrl = new URL(`http://127.0.0.1:${port}/mcp`);
|
||||
const authServerUrl = new URL(`http://127.0.0.1:${AUTH_PORT}/`);
|
||||
|
||||
// Demo confidential client. DEMO ONLY — never hard-code real credentials.
|
||||
export const DEMO_CLIENT = { clientId: 'demo-m2m-client', clientSecret: 'demo-m2m-secret', allowedScopes: ['mcp:tools', 'mcp:read'] };
|
||||
|
||||
// ---- Authorization Server (client_credentials only) ----
|
||||
const as = createClientCredentialsAuthServer({ authServerUrl, clients: [DEMO_CLIENT] });
|
||||
as.app.listen(AUTH_PORT, () => console.error(`[auth-server] client_credentials AS on ${authServerUrl.href}`));
|
||||
|
||||
// ---- Resource Server (MCP) ----
|
||||
const handler = createMcpHandler(ctx => {
|
||||
const server = new McpServer({ name: 'oauth-client-credentials-example', version: '1.0.0' });
|
||||
server.registerTool(
|
||||
'whoami',
|
||||
{ description: 'Returns the authenticated client and its granted scopes.', inputSchema: z.object({}) },
|
||||
async () => ({
|
||||
content: [{ type: 'text', text: JSON.stringify({ clientId: ctx.authInfo?.clientId, scopes: ctx.authInfo?.scopes }) }]
|
||||
})
|
||||
);
|
||||
return server;
|
||||
});
|
||||
|
||||
const app = createMcpExpressApp();
|
||||
app.use(
|
||||
mcpAuthMetadataRouter({
|
||||
oauthMetadata: as.metadata,
|
||||
resourceServerUrl: mcpServerUrl,
|
||||
scopesSupported: ['mcp:tools', 'mcp:read'],
|
||||
resourceName: 'oauth-client-credentials example'
|
||||
})
|
||||
);
|
||||
const auth = requireBearerAuth({
|
||||
verifier: as.verifier,
|
||||
requiredScopes: ['mcp:tools'],
|
||||
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl)
|
||||
});
|
||||
// `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(`[resource-server] MCP on ${mcpServerUrl.href}`));
|
||||
Loading…
Reference in a new issue