@contractspec/bundle.library 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,36 +6,11 @@ import type {
6
6
  import { PresentationRegistry } from '@contractspec/lib.contracts-spec/presentations';
7
7
  import { createMcpServer } from '@contractspec/lib.contracts-runtime-server-mcp/provider-mcp';
8
8
  import type { PresentationSpec } from '@contractspec/lib.contracts-spec/presentations';
9
- import { mcp } from 'elysia-mcp';
9
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
10
+ import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';
11
+ import { Elysia } from 'elysia';
10
12
  import { Logger } from '@contractspec/lib.logger';
11
-
12
- function createConsoleLikeLogger(logger: Logger) {
13
- const isDebug = process.env.CONTRACTSPEC_MCP_DEBUG === '1';
14
-
15
- const toMessage = (args: unknown[]) =>
16
- args.map((a) => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ');
17
-
18
- return {
19
- log: (...args: unknown[]) => {
20
- if (!isDebug) return;
21
- logger.info(toMessage(args));
22
- },
23
- info: (...args: unknown[]) => {
24
- if (!isDebug) return;
25
- logger.info(toMessage(args));
26
- },
27
- warn: (...args: unknown[]) => {
28
- logger.warn(toMessage(args));
29
- },
30
- error: (...args: unknown[]) => {
31
- logger.error(toMessage(args));
32
- },
33
- debug: (...args: unknown[]) => {
34
- if (!isDebug) return;
35
- logger.debug(toMessage(args));
36
- },
37
- };
38
- }
13
+ import { randomUUID } from 'node:crypto';
39
14
 
40
15
  interface McpHttpHandlerConfig {
41
16
  path: string;
@@ -52,75 +27,193 @@ const baseCtx = {
52
27
  decide: async () => ({ effect: 'allow' as const }),
53
28
  };
54
29
 
55
- // export function createMcpNextjsHandler({
56
- // path,
57
- // serverName,
58
- // ops,
59
- // resources,
60
- // prompts,
61
- // presentationsV2,
62
- // }: McpHttpHandlerConfig) {
63
- // return createMcpHandler(
64
- // (server) => {
65
- // createMcpServer(server, ops, resources, prompts, {
66
- // toolCtx: () => baseCtx,
67
- // promptCtx: () => ({ locale: 'en' }),
68
- // resourceCtx: () => ({ locale: 'en' }),
69
- // presentationsV2,
70
- // });
71
- // },
72
- // {
73
- // serverInfo: {
74
- // name: serverName,
75
- // version: '1.0.0',
76
- // },
77
- // },
78
- // {
79
- // // basePath: path,
80
- // disableSse: true,
81
- // verboseLogs: true,
82
- // }
83
- // );
84
- // }
30
+ interface McpSessionState {
31
+ server: McpServer;
32
+ transport: WebStandardStreamableHTTPServerTransport;
33
+ }
85
34
 
86
- export function createMcpElysiaHandler({
35
+ function createJsonRpcErrorResponse(
36
+ status: number,
37
+ code: number,
38
+ message: string,
39
+ data?: string
40
+ ) {
41
+ return new Response(
42
+ JSON.stringify({
43
+ jsonrpc: '2.0',
44
+ error: {
45
+ code,
46
+ message,
47
+ ...(data ? { data } : {}),
48
+ },
49
+ id: null,
50
+ }),
51
+ {
52
+ status,
53
+ headers: {
54
+ 'content-type': 'application/json',
55
+ },
56
+ }
57
+ );
58
+ }
59
+
60
+ function createSessionState({
87
61
  logger,
88
- path,
89
62
  serverName,
90
63
  ops,
91
64
  resources,
92
65
  prompts,
93
66
  presentations,
94
- }: McpHttpHandlerConfig) {
95
- logger.info('Setting up MCP handler...');
96
- return mcp({
97
- basePath: path,
98
- // Do NOT use console.* in production paths; adapt to console-like interface for the plugin.
99
- logger: createConsoleLikeLogger(logger),
100
- serverInfo: {
67
+ stateful,
68
+ }: McpHttpHandlerConfig & { stateful: boolean }): Promise<McpSessionState> {
69
+ const server = new McpServer(
70
+ {
101
71
  name: serverName,
102
72
  version: '1.0.0',
103
73
  },
104
- // Cursor MCP HTTP clients may not persist Mcp-Session-Id headers reliably.
105
- // Run in stateless + JSON response mode by default to maximize compatibility.
106
- // Set CONTRACTSPEC_MCP_STATEFUL=1 to restore sessionful behavior.
107
- stateless: process.env.CONTRACTSPEC_MCP_STATEFUL !== '1',
74
+ {
75
+ capabilities: {
76
+ tools: {},
77
+ resources: {},
78
+ prompts: {},
79
+ logging: {},
80
+ },
81
+ }
82
+ );
83
+
84
+ logger.info('Setting up MCP server...');
85
+ createMcpServer(server, ops, resources, prompts, {
86
+ logger,
87
+ toolCtx: () => baseCtx,
88
+ promptCtx: () => ({ locale: 'en' }),
89
+ resourceCtx: () => ({ locale: 'en' }),
90
+ presentations: new PresentationRegistry(presentations),
91
+ });
92
+
93
+ const transport = new WebStandardStreamableHTTPServerTransport({
94
+ sessionIdGenerator: stateful ? () => randomUUID() : undefined,
108
95
  enableJsonResponse: true,
109
- capabilities: {
110
- tools: {},
111
- resources: {},
112
- prompts: {},
113
- logging: {},
114
- },
115
- setupServer: (server) => {
116
- logger.info('Setting up MCP server...');
117
- createMcpServer(server, ops, resources, prompts, {
96
+ });
97
+
98
+ return server.connect(transport).then(() => ({ server, transport }));
99
+ }
100
+
101
+ async function closeSessionState(state: McpSessionState) {
102
+ await Promise.allSettled([state.transport.close(), state.server.close()]);
103
+ }
104
+
105
+ function toErrorMessage(error: unknown) {
106
+ return error instanceof Error
107
+ ? (error.stack ?? error.message)
108
+ : String(error);
109
+ }
110
+
111
+ export function createMcpElysiaHandler({
112
+ logger,
113
+ path,
114
+ serverName,
115
+ ops,
116
+ resources,
117
+ prompts,
118
+ presentations,
119
+ }: McpHttpHandlerConfig) {
120
+ logger.info('Setting up MCP handler...');
121
+
122
+ const isStateful = process.env.CONTRACTSPEC_MCP_STATEFUL === '1';
123
+ const sessions = new Map<string, McpSessionState>();
124
+
125
+ async function handleStateless(request: Request) {
126
+ const state = await createSessionState({
127
+ logger,
128
+ path,
129
+ serverName,
130
+ ops,
131
+ resources,
132
+ prompts,
133
+ presentations,
134
+ stateful: false,
135
+ });
136
+
137
+ try {
138
+ return await state.transport.handleRequest(request);
139
+ } finally {
140
+ await closeSessionState(state);
141
+ }
142
+ }
143
+
144
+ async function closeSession(sessionId: string) {
145
+ const state = sessions.get(sessionId);
146
+ if (!state) return;
147
+ sessions.delete(sessionId);
148
+ await closeSessionState(state);
149
+ }
150
+
151
+ async function handleStateful(request: Request) {
152
+ const requestedSessionId = request.headers.get('mcp-session-id');
153
+ let state: McpSessionState;
154
+ let createdState = false;
155
+
156
+ if (requestedSessionId) {
157
+ const existing = sessions.get(requestedSessionId);
158
+ if (!existing) {
159
+ return createJsonRpcErrorResponse(404, -32001, 'Session not found');
160
+ }
161
+ state = existing;
162
+ } else {
163
+ state = await createSessionState({
118
164
  logger,
119
- toolCtx: () => baseCtx,
120
- promptCtx: () => ({ locale: 'en' }),
121
- resourceCtx: () => ({ locale: 'en' }),
122
- presentations: new PresentationRegistry(presentations),
165
+ path,
166
+ serverName,
167
+ ops,
168
+ resources,
169
+ prompts,
170
+ presentations,
171
+ stateful: true,
123
172
  });
124
- },
125
- });
173
+ createdState = true;
174
+ }
175
+
176
+ try {
177
+ const response = await state.transport.handleRequest(request);
178
+ const activeSessionId = state.transport.sessionId;
179
+
180
+ if (activeSessionId && !sessions.has(activeSessionId)) {
181
+ sessions.set(activeSessionId, state);
182
+ }
183
+
184
+ if (request.method === 'DELETE' && activeSessionId) {
185
+ await closeSession(activeSessionId);
186
+ } else if (!activeSessionId && createdState) {
187
+ await closeSessionState(state);
188
+ }
189
+
190
+ return response;
191
+ } catch (error) {
192
+ if (createdState) {
193
+ await closeSessionState(state);
194
+ }
195
+ throw error;
196
+ }
197
+ }
198
+
199
+ return new Elysia({ name: `mcp-${serverName}` }).all(
200
+ path,
201
+ async ({ request }) => {
202
+ try {
203
+ if (isStateful) {
204
+ return await handleStateful(request);
205
+ }
206
+
207
+ return await handleStateless(request);
208
+ } catch (error) {
209
+ logger.error('Error handling MCP request', {
210
+ path,
211
+ method: request.method,
212
+ error: toErrorMessage(error),
213
+ });
214
+
215
+ return createJsonRpcErrorResponse(500, -32000, 'Internal error');
216
+ }
217
+ }
218
+ );
126
219
  }