cloudflare/cloudflare-typescript

Public

mirrored fromhttps://github.com/cloudflare/cloudflare-typescriptAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v7

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

packages/mcp-server/src/http.ts

227lines · modecode

1// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2
3import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
4import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
5import { ClientOptions } from 'cloudflare';
6import express from 'express';
7import pino from 'pino';
8import pinoHttp from 'pino-http';
9import { getStainlessApiKey, parseClientAuthHeaders } from './auth';
10import { getLogger } from './logger';
11import { McpOptions } from './options';
12import { initMcpServer, newMcpServer } from './server';
13
14const newServer = async ({
15 clientOptions,
16 mcpOptions,
17 req,
18 res,
19}: {
20 clientOptions: ClientOptions;
21 mcpOptions: McpOptions;
22 req: express.Request;
23 res: express.Response;
24}): Promise<McpServer | null> => {
25 const stainlessApiKey = getStainlessApiKey(req, mcpOptions);
26 const customInstructionsPath = mcpOptions.customInstructionsPath;
27 const server = await newMcpServer({ stainlessApiKey, customInstructionsPath });
28
29 const authOptions = parseClientAuthHeaders(req, false);
30
31 let upstreamClientEnvs: Record<string, string> | undefined;
32 const clientEnvsHeader = req.headers['x-stainless-mcp-client-envs'];
33 if (typeof clientEnvsHeader === 'string') {
34 try {
35 const parsed = JSON.parse(clientEnvsHeader);
36 if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
37 upstreamClientEnvs = parsed;
38 }
39 } catch {
40 // Ignore malformed header
41 }
42 }
43
44 // Parse x-stainless-mcp-client-permissions header to override permission options
45 //
46 // Note: Permissions are best-effort and intended to prevent clients from doing unexpected things;
47 // they're not a hard security boundary, so we allow arbitrary, client-driven overrides.
48 //
49 // See the Stainless MCP documentation for more details.
50 let effectiveMcpOptions = mcpOptions;
51 const clientPermissionsHeader = req.headers['x-stainless-mcp-client-permissions'];
52 if (typeof clientPermissionsHeader === 'string') {
53 try {
54 const parsed = JSON.parse(clientPermissionsHeader);
55 if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
56 effectiveMcpOptions = {
57 ...mcpOptions,
58 ...(typeof parsed.allow_http_gets === 'boolean' && { codeAllowHttpGets: parsed.allow_http_gets }),
59 ...(Array.isArray(parsed.allowed_methods) && { codeAllowedMethods: parsed.allowed_methods }),
60 ...(Array.isArray(parsed.blocked_methods) && { codeBlockedMethods: parsed.blocked_methods }),
61 };
62 getLogger().info(
63 { clientPermissions: parsed },
64 'Overriding code execution permissions from x-stainless-mcp-client-permissions header',
65 );
66 }
67 } catch (error) {
68 getLogger().warn({ error }, 'Failed to parse x-stainless-mcp-client-permissions header');
69 }
70 }
71
72 const mcpClientInfo =
73 typeof req.body?.params?.clientInfo?.name === 'string' ?
74 { name: req.body.params.clientInfo.name, version: String(req.body.params.clientInfo.version ?? '') }
75 : undefined;
76
77 await initMcpServer({
78 server: server,
79 mcpOptions: effectiveMcpOptions,
80 clientOptions: {
81 ...clientOptions,
82 ...authOptions,
83 },
84 stainlessApiKey: stainlessApiKey,
85 upstreamClientEnvs,
86 mcpSessionId: (req as any).mcpSessionId,
87 mcpClientInfo,
88 });
89
90 if (mcpClientInfo) {
91 getLogger().info({ mcpSessionId: (req as any).mcpSessionId, mcpClientInfo }, 'MCP client connected');
92 }
93
94 return server;
95};
96
97const post =
98 (options: { clientOptions: ClientOptions; mcpOptions: McpOptions }) =>
99 async (req: express.Request, res: express.Response) => {
100 const server = await newServer({ ...options, req, res });
101 // If we return null, we already set the authorization error.
102 if (server === null) return;
103 const transport = new StreamableHTTPServerTransport();
104 await server.connect(transport as any);
105 await transport.handleRequest(req, res, req.body);
106 };
107
108const get = async (req: express.Request, res: express.Response) => {
109 res.status(405).json({
110 jsonrpc: '2.0',
111 error: {
112 code: -32000,
113 message: 'Method not supported',
114 },
115 });
116};
117
118const del = async (req: express.Request, res: express.Response) => {
119 res.status(405).json({
120 jsonrpc: '2.0',
121 error: {
122 code: -32000,
123 message: 'Method not supported',
124 },
125 });
126};
127
128const redactHeaders = (headers: Record<string, any>) => {
129 const hiddenHeaders = /auth|cookie|key|token|x-stainless-mcp-client-envs/i;
130 const filtered = { ...headers };
131 Object.keys(filtered).forEach((key) => {
132 if (hiddenHeaders.test(key)) {
133 filtered[key] = '[REDACTED]';
134 }
135 });
136 return filtered;
137};
138
139export const streamableHTTPApp = ({
140 clientOptions = {},
141 mcpOptions,
142}: {
143 clientOptions?: ClientOptions;
144 mcpOptions: McpOptions;
145}): express.Express => {
146 const app = express();
147 app.set('query parser', 'extended');
148 app.use(express.json());
149 app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
150 const existing = req.headers['mcp-session-id'];
151 const sessionId = (Array.isArray(existing) ? existing[0] : existing) || crypto.randomUUID();
152 (req as any).mcpSessionId = sessionId;
153 const origWriteHead = res.writeHead.bind(res);
154 res.writeHead = function (statusCode: number, ...rest: any[]) {
155 res.setHeader('mcp-session-id', sessionId);
156 return origWriteHead(statusCode, ...rest);
157 } as typeof res.writeHead;
158 next();
159 });
160 app.use(
161 pinoHttp({
162 logger: getLogger(),
163 customProps: (req) => ({
164 mcpSessionId: (req as any).mcpSessionId,
165 }),
166 customLogLevel: (req, res) => {
167 if (res.statusCode >= 500) {
168 return 'error';
169 } else if (res.statusCode >= 400) {
170 return 'warn';
171 }
172 return 'info';
173 },
174 customSuccessMessage: function (req, res) {
175 return `Request ${req.method} to ${req.url} completed with status ${res.statusCode}`;
176 },
177 customErrorMessage: function (req, res, err) {
178 return `Request ${req.method} to ${req.url} errored with status ${res.statusCode}`;
179 },
180 serializers: {
181 req: pino.stdSerializers.wrapRequestSerializer((req) => {
182 return {
183 ...req,
184 headers: redactHeaders(req.raw.headers),
185 };
186 }),
187 res: pino.stdSerializers.wrapResponseSerializer((res) => {
188 return {
189 ...res,
190 headers: redactHeaders(res.headers),
191 };
192 }),
193 },
194 }),
195 );
196
197 app.get('/health', async (req: express.Request, res: express.Response) => {
198 res.status(200).send('OK');
199 });
200 app.get('/', get);
201 app.post('/', post({ clientOptions, mcpOptions }));
202 app.delete('/', del);
203
204 return app;
205};
206
207export const launchStreamableHTTPServer = async ({
208 mcpOptions,
209 port,
210}: {
211 mcpOptions: McpOptions;
212 port: number | string | undefined;
213}) => {
214 const app = streamableHTTPApp({ mcpOptions });
215 const server = app.listen(port);
216 const address = server.address();
217
218 const logger = getLogger();
219
220 if (typeof address === 'string') {
221 logger.info(`MCP Server running on streamable HTTP at ${address}`);
222 } else if (address !== null) {
223 logger.info(`MCP Server running on streamable HTTP on port ${address.port}`);
224 } else {
225 logger.info(`MCP Server running on streamable HTTP on port ${port}`);
226 }
227};
228