Skip to content

Commit d93bb2d

Browse files
chore(internal): fix MCP cloudflare worker initialization
1 parent c7668e7 commit d93bb2d

1 file changed

Lines changed: 66 additions & 9 deletions

File tree

  • packages/mcp-server/cloudflare-worker/src

packages/mcp-server/cloudflare-worker/src/index.ts

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ import { makeOAuthConsent } from './app';
66
// `instanceof McpServer` check fails because the two `McpServer` classes are
77
// distinct constructors.
88
import { McpAgent } from 'agents/mcp';
9+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
910
import OAuthProvider from '@cloudflare/workers-oauth-provider';
1011
import { ClientOptions } from '@imagekit/nodejs';
1112
import { McpOptions } from '@imagekit/api-mcp/options';
1213
import { initMcpServer, newMcpServer } from '@imagekit/api-mcp/server';
14+
import { configureLogger } from '@imagekit/api-mcp/logger';
1315
import type { ExportedHandler } from '@cloudflare/workers-types';
1416

1517
type MCPProps = {
@@ -58,19 +60,74 @@ const serverConfig: ServerConfig = {
5860
],
5961
};
6062

63+
// `newMcpServer` fetches MCP server instructions from the Stainless API. In a
64+
// Durable Object, that fetch happens inside `blockConcurrencyWhile`; if it
65+
// hangs the DO is reset, and if it rejects the same thing happens. Race
66+
// against a short timeout and catch any rejection so any failure mode lands
67+
// on a fallback server constructed without instructions (the `initialize`
68+
// response simply omits the `instructions` field, which is spec-allowed).
69+
const INSTRUCTIONS_FETCH_TIMEOUT_MS = 5000;
70+
71+
function fallbackMcpServer(): McpServer {
72+
return new McpServer(
73+
{ name: 'imagekit_nodejs_api', version: '7.5.0' },
74+
{ capabilities: { tools: {}, logging: {} } },
75+
);
76+
}
77+
78+
async function buildMcpServer(stainlessApiKey?: string): Promise<McpServer> {
79+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
80+
try {
81+
const fetched = newMcpServer({ stainlessApiKey });
82+
const timeout = new Promise<null>((resolve) => {
83+
timeoutId = setTimeout(() => resolve(null), INSTRUCTIONS_FETCH_TIMEOUT_MS);
84+
});
85+
86+
const result = await Promise.race([fetched, timeout]);
87+
88+
if (result != null) {
89+
return result;
90+
}
91+
} catch (error) {
92+
console.error('Failed to build MCP server from upstream instructions; using fallback', error);
93+
} finally {
94+
if (timeoutId != null) {
95+
clearTimeout(timeoutId);
96+
}
97+
}
98+
99+
return fallbackMcpServer();
100+
}
101+
61102
export class MyMCP extends McpAgent<Env, unknown, MCPProps> {
62-
server = newMcpServer({});
103+
#resolveServer!: (server: McpServer) => void;
104+
#rejectServer!: (error: unknown) => void;
105+
server: Promise<McpServer> = new Promise<McpServer>((resolve, reject) => {
106+
this.#resolveServer = resolve;
107+
this.#rejectServer = reject;
108+
});
63109

64110
async init() {
65-
if (this.props == null) {
66-
throw new Error('MCP props are not initialized');
67-
}
111+
try {
112+
if (this.props == null) {
113+
throw new Error('MCP props are not initialized');
114+
}
68115

69-
initMcpServer({
70-
server: await this.server,
71-
clientOptions: this.props.clientProps,
72-
mcpOptions: this.props.clientConfig,
73-
});
116+
configureLogger({ level: 'info', pretty: false });
117+
118+
const server = await buildMcpServer(this.props.clientConfig?.stainlessApiKey);
119+
120+
await initMcpServer({
121+
server,
122+
clientOptions: this.props.clientProps,
123+
mcpOptions: this.props.clientConfig,
124+
});
125+
126+
this.#resolveServer(server);
127+
} catch (error) {
128+
this.#rejectServer(error);
129+
throw error;
130+
}
74131
}
75132
}
76133

0 commit comments

Comments
 (0)