@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.
@@ -1,72 +1,161 @@
1
1
  // src/application/mcp/common.ts
2
2
  import { PresentationRegistry } from "@contractspec/lib.contracts-spec/presentations";
3
3
  import { createMcpServer } from "@contractspec/lib.contracts-runtime-server-mcp/provider-mcp";
4
- import { mcp } from "elysia-mcp";
5
- function createConsoleLikeLogger(logger) {
6
- const isDebug = process.env.CONTRACTSPEC_MCP_DEBUG === "1";
7
- const toMessage = (args) => args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
8
- return {
9
- log: (...args) => {
10
- if (!isDebug)
11
- return;
12
- logger.info(toMessage(args));
13
- },
14
- info: (...args) => {
15
- if (!isDebug)
16
- return;
17
- logger.info(toMessage(args));
18
- },
19
- warn: (...args) => {
20
- logger.warn(toMessage(args));
21
- },
22
- error: (...args) => {
23
- logger.error(toMessage(args));
24
- },
25
- debug: (...args) => {
26
- if (!isDebug)
27
- return;
28
- logger.debug(toMessage(args));
29
- }
30
- };
31
- }
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
6
+ import { Elysia } from "elysia";
7
+ import { randomUUID } from "node:crypto";
32
8
  var baseCtx = {
33
9
  actor: "anonymous",
34
10
  decide: async () => ({ effect: "allow" })
35
11
  };
36
- function createMcpElysiaHandler({
12
+ function createJsonRpcErrorResponse(status, code, message, data) {
13
+ return new Response(JSON.stringify({
14
+ jsonrpc: "2.0",
15
+ error: {
16
+ code,
17
+ message,
18
+ ...data ? { data } : {}
19
+ },
20
+ id: null
21
+ }), {
22
+ status,
23
+ headers: {
24
+ "content-type": "application/json"
25
+ }
26
+ });
27
+ }
28
+ function createSessionState({
37
29
  logger,
38
- path,
39
30
  serverName,
40
31
  ops,
41
32
  resources,
42
33
  prompts,
43
- presentations
34
+ presentations,
35
+ stateful
44
36
  }) {
45
- logger.info("Setting up MCP handler...");
46
- return mcp({
47
- basePath: path,
48
- logger: createConsoleLikeLogger(logger),
49
- serverInfo: {
50
- name: serverName,
51
- version: "1.0.0"
52
- },
53
- stateless: process.env.CONTRACTSPEC_MCP_STATEFUL !== "1",
54
- enableJsonResponse: true,
37
+ const server = new McpServer({
38
+ name: serverName,
39
+ version: "1.0.0"
40
+ }, {
55
41
  capabilities: {
56
42
  tools: {},
57
43
  resources: {},
58
44
  prompts: {},
59
45
  logging: {}
60
- },
61
- setupServer: (server) => {
62
- logger.info("Setting up MCP server...");
63
- createMcpServer(server, ops, resources, prompts, {
46
+ }
47
+ });
48
+ logger.info("Setting up MCP server...");
49
+ createMcpServer(server, ops, resources, prompts, {
50
+ logger,
51
+ toolCtx: () => baseCtx,
52
+ promptCtx: () => ({ locale: "en" }),
53
+ resourceCtx: () => ({ locale: "en" }),
54
+ presentations: new PresentationRegistry(presentations)
55
+ });
56
+ const transport = new WebStandardStreamableHTTPServerTransport({
57
+ sessionIdGenerator: stateful ? () => randomUUID() : undefined,
58
+ enableJsonResponse: true
59
+ });
60
+ return server.connect(transport).then(() => ({ server, transport }));
61
+ }
62
+ async function closeSessionState(state) {
63
+ await Promise.allSettled([state.transport.close(), state.server.close()]);
64
+ }
65
+ function toErrorMessage(error) {
66
+ return error instanceof Error ? error.stack ?? error.message : String(error);
67
+ }
68
+ function createMcpElysiaHandler({
69
+ logger,
70
+ path,
71
+ serverName,
72
+ ops,
73
+ resources,
74
+ prompts,
75
+ presentations
76
+ }) {
77
+ logger.info("Setting up MCP handler...");
78
+ const isStateful = process.env.CONTRACTSPEC_MCP_STATEFUL === "1";
79
+ const sessions = new Map;
80
+ async function handleStateless(request) {
81
+ const state = await createSessionState({
82
+ logger,
83
+ path,
84
+ serverName,
85
+ ops,
86
+ resources,
87
+ prompts,
88
+ presentations,
89
+ stateful: false
90
+ });
91
+ try {
92
+ return await state.transport.handleRequest(request);
93
+ } finally {
94
+ await closeSessionState(state);
95
+ }
96
+ }
97
+ async function closeSession(sessionId) {
98
+ const state = sessions.get(sessionId);
99
+ if (!state)
100
+ return;
101
+ sessions.delete(sessionId);
102
+ await closeSessionState(state);
103
+ }
104
+ async function handleStateful(request) {
105
+ const requestedSessionId = request.headers.get("mcp-session-id");
106
+ let state;
107
+ let createdState = false;
108
+ if (requestedSessionId) {
109
+ const existing = sessions.get(requestedSessionId);
110
+ if (!existing) {
111
+ return createJsonRpcErrorResponse(404, -32001, "Session not found");
112
+ }
113
+ state = existing;
114
+ } else {
115
+ state = await createSessionState({
64
116
  logger,
65
- toolCtx: () => baseCtx,
66
- promptCtx: () => ({ locale: "en" }),
67
- resourceCtx: () => ({ locale: "en" }),
68
- presentations: new PresentationRegistry(presentations)
117
+ path,
118
+ serverName,
119
+ ops,
120
+ resources,
121
+ prompts,
122
+ presentations,
123
+ stateful: true
124
+ });
125
+ createdState = true;
126
+ }
127
+ try {
128
+ const response = await state.transport.handleRequest(request);
129
+ const activeSessionId = state.transport.sessionId;
130
+ if (activeSessionId && !sessions.has(activeSessionId)) {
131
+ sessions.set(activeSessionId, state);
132
+ }
133
+ if (request.method === "DELETE" && activeSessionId) {
134
+ await closeSession(activeSessionId);
135
+ } else if (!activeSessionId && createdState) {
136
+ await closeSessionState(state);
137
+ }
138
+ return response;
139
+ } catch (error) {
140
+ if (createdState) {
141
+ await closeSessionState(state);
142
+ }
143
+ throw error;
144
+ }
145
+ }
146
+ return new Elysia({ name: `mcp-${serverName}` }).all(path, async ({ request }) => {
147
+ try {
148
+ if (isStateful) {
149
+ return await handleStateful(request);
150
+ }
151
+ return await handleStateless(request);
152
+ } catch (error) {
153
+ logger.error("Error handling MCP request", {
154
+ path,
155
+ method: request.method,
156
+ error: toErrorMessage(error)
69
157
  });
158
+ return createJsonRpcErrorResponse(500, -32000, "Internal error");
70
159
  }
71
160
  });
72
161
  }
@@ -1,72 +1,161 @@
1
1
  // src/application/mcp/common.ts
2
2
  import { PresentationRegistry } from "@contractspec/lib.contracts-spec/presentations";
3
3
  import { createMcpServer } from "@contractspec/lib.contracts-runtime-server-mcp/provider-mcp";
4
- import { mcp } from "elysia-mcp";
5
- function createConsoleLikeLogger(logger) {
6
- const isDebug = process.env.CONTRACTSPEC_MCP_DEBUG === "1";
7
- const toMessage = (args) => args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
8
- return {
9
- log: (...args) => {
10
- if (!isDebug)
11
- return;
12
- logger.info(toMessage(args));
13
- },
14
- info: (...args) => {
15
- if (!isDebug)
16
- return;
17
- logger.info(toMessage(args));
18
- },
19
- warn: (...args) => {
20
- logger.warn(toMessage(args));
21
- },
22
- error: (...args) => {
23
- logger.error(toMessage(args));
24
- },
25
- debug: (...args) => {
26
- if (!isDebug)
27
- return;
28
- logger.debug(toMessage(args));
29
- }
30
- };
31
- }
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
6
+ import { Elysia } from "elysia";
7
+ import { randomUUID } from "node:crypto";
32
8
  var baseCtx = {
33
9
  actor: "anonymous",
34
10
  decide: async () => ({ effect: "allow" })
35
11
  };
36
- function createMcpElysiaHandler({
12
+ function createJsonRpcErrorResponse(status, code, message, data) {
13
+ return new Response(JSON.stringify({
14
+ jsonrpc: "2.0",
15
+ error: {
16
+ code,
17
+ message,
18
+ ...data ? { data } : {}
19
+ },
20
+ id: null
21
+ }), {
22
+ status,
23
+ headers: {
24
+ "content-type": "application/json"
25
+ }
26
+ });
27
+ }
28
+ function createSessionState({
37
29
  logger,
38
- path,
39
30
  serverName,
40
31
  ops,
41
32
  resources,
42
33
  prompts,
43
- presentations
34
+ presentations,
35
+ stateful
44
36
  }) {
45
- logger.info("Setting up MCP handler...");
46
- return mcp({
47
- basePath: path,
48
- logger: createConsoleLikeLogger(logger),
49
- serverInfo: {
50
- name: serverName,
51
- version: "1.0.0"
52
- },
53
- stateless: process.env.CONTRACTSPEC_MCP_STATEFUL !== "1",
54
- enableJsonResponse: true,
37
+ const server = new McpServer({
38
+ name: serverName,
39
+ version: "1.0.0"
40
+ }, {
55
41
  capabilities: {
56
42
  tools: {},
57
43
  resources: {},
58
44
  prompts: {},
59
45
  logging: {}
60
- },
61
- setupServer: (server) => {
62
- logger.info("Setting up MCP server...");
63
- createMcpServer(server, ops, resources, prompts, {
46
+ }
47
+ });
48
+ logger.info("Setting up MCP server...");
49
+ createMcpServer(server, ops, resources, prompts, {
50
+ logger,
51
+ toolCtx: () => baseCtx,
52
+ promptCtx: () => ({ locale: "en" }),
53
+ resourceCtx: () => ({ locale: "en" }),
54
+ presentations: new PresentationRegistry(presentations)
55
+ });
56
+ const transport = new WebStandardStreamableHTTPServerTransport({
57
+ sessionIdGenerator: stateful ? () => randomUUID() : undefined,
58
+ enableJsonResponse: true
59
+ });
60
+ return server.connect(transport).then(() => ({ server, transport }));
61
+ }
62
+ async function closeSessionState(state) {
63
+ await Promise.allSettled([state.transport.close(), state.server.close()]);
64
+ }
65
+ function toErrorMessage(error) {
66
+ return error instanceof Error ? error.stack ?? error.message : String(error);
67
+ }
68
+ function createMcpElysiaHandler({
69
+ logger,
70
+ path,
71
+ serverName,
72
+ ops,
73
+ resources,
74
+ prompts,
75
+ presentations
76
+ }) {
77
+ logger.info("Setting up MCP handler...");
78
+ const isStateful = process.env.CONTRACTSPEC_MCP_STATEFUL === "1";
79
+ const sessions = new Map;
80
+ async function handleStateless(request) {
81
+ const state = await createSessionState({
82
+ logger,
83
+ path,
84
+ serverName,
85
+ ops,
86
+ resources,
87
+ prompts,
88
+ presentations,
89
+ stateful: false
90
+ });
91
+ try {
92
+ return await state.transport.handleRequest(request);
93
+ } finally {
94
+ await closeSessionState(state);
95
+ }
96
+ }
97
+ async function closeSession(sessionId) {
98
+ const state = sessions.get(sessionId);
99
+ if (!state)
100
+ return;
101
+ sessions.delete(sessionId);
102
+ await closeSessionState(state);
103
+ }
104
+ async function handleStateful(request) {
105
+ const requestedSessionId = request.headers.get("mcp-session-id");
106
+ let state;
107
+ let createdState = false;
108
+ if (requestedSessionId) {
109
+ const existing = sessions.get(requestedSessionId);
110
+ if (!existing) {
111
+ return createJsonRpcErrorResponse(404, -32001, "Session not found");
112
+ }
113
+ state = existing;
114
+ } else {
115
+ state = await createSessionState({
64
116
  logger,
65
- toolCtx: () => baseCtx,
66
- promptCtx: () => ({ locale: "en" }),
67
- resourceCtx: () => ({ locale: "en" }),
68
- presentations: new PresentationRegistry(presentations)
117
+ path,
118
+ serverName,
119
+ ops,
120
+ resources,
121
+ prompts,
122
+ presentations,
123
+ stateful: true
124
+ });
125
+ createdState = true;
126
+ }
127
+ try {
128
+ const response = await state.transport.handleRequest(request);
129
+ const activeSessionId = state.transport.sessionId;
130
+ if (activeSessionId && !sessions.has(activeSessionId)) {
131
+ sessions.set(activeSessionId, state);
132
+ }
133
+ if (request.method === "DELETE" && activeSessionId) {
134
+ await closeSession(activeSessionId);
135
+ } else if (!activeSessionId && createdState) {
136
+ await closeSessionState(state);
137
+ }
138
+ return response;
139
+ } catch (error) {
140
+ if (createdState) {
141
+ await closeSessionState(state);
142
+ }
143
+ throw error;
144
+ }
145
+ }
146
+ return new Elysia({ name: `mcp-${serverName}` }).all(path, async ({ request }) => {
147
+ try {
148
+ if (isStateful) {
149
+ return await handleStateful(request);
150
+ }
151
+ return await handleStateless(request);
152
+ } catch (error) {
153
+ logger.error("Error handling MCP request", {
154
+ path,
155
+ method: request.method,
156
+ error: toErrorMessage(error)
69
157
  });
158
+ return createJsonRpcErrorResponse(500, -32000, "Internal error");
70
159
  }
71
160
  });
72
161
  }
@@ -1,72 +1,161 @@
1
1
  // src/application/mcp/common.ts
2
2
  import { PresentationRegistry } from "@contractspec/lib.contracts-spec/presentations";
3
3
  import { createMcpServer } from "@contractspec/lib.contracts-runtime-server-mcp/provider-mcp";
4
- import { mcp } from "elysia-mcp";
5
- function createConsoleLikeLogger(logger) {
6
- const isDebug = process.env.CONTRACTSPEC_MCP_DEBUG === "1";
7
- const toMessage = (args) => args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ");
8
- return {
9
- log: (...args) => {
10
- if (!isDebug)
11
- return;
12
- logger.info(toMessage(args));
13
- },
14
- info: (...args) => {
15
- if (!isDebug)
16
- return;
17
- logger.info(toMessage(args));
18
- },
19
- warn: (...args) => {
20
- logger.warn(toMessage(args));
21
- },
22
- error: (...args) => {
23
- logger.error(toMessage(args));
24
- },
25
- debug: (...args) => {
26
- if (!isDebug)
27
- return;
28
- logger.debug(toMessage(args));
29
- }
30
- };
31
- }
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
6
+ import { Elysia } from "elysia";
7
+ import { randomUUID } from "node:crypto";
32
8
  var baseCtx = {
33
9
  actor: "anonymous",
34
10
  decide: async () => ({ effect: "allow" })
35
11
  };
36
- function createMcpElysiaHandler({
12
+ function createJsonRpcErrorResponse(status, code, message, data) {
13
+ return new Response(JSON.stringify({
14
+ jsonrpc: "2.0",
15
+ error: {
16
+ code,
17
+ message,
18
+ ...data ? { data } : {}
19
+ },
20
+ id: null
21
+ }), {
22
+ status,
23
+ headers: {
24
+ "content-type": "application/json"
25
+ }
26
+ });
27
+ }
28
+ function createSessionState({
37
29
  logger,
38
- path,
39
30
  serverName,
40
31
  ops,
41
32
  resources,
42
33
  prompts,
43
- presentations
34
+ presentations,
35
+ stateful
44
36
  }) {
45
- logger.info("Setting up MCP handler...");
46
- return mcp({
47
- basePath: path,
48
- logger: createConsoleLikeLogger(logger),
49
- serverInfo: {
50
- name: serverName,
51
- version: "1.0.0"
52
- },
53
- stateless: process.env.CONTRACTSPEC_MCP_STATEFUL !== "1",
54
- enableJsonResponse: true,
37
+ const server = new McpServer({
38
+ name: serverName,
39
+ version: "1.0.0"
40
+ }, {
55
41
  capabilities: {
56
42
  tools: {},
57
43
  resources: {},
58
44
  prompts: {},
59
45
  logging: {}
60
- },
61
- setupServer: (server) => {
62
- logger.info("Setting up MCP server...");
63
- createMcpServer(server, ops, resources, prompts, {
46
+ }
47
+ });
48
+ logger.info("Setting up MCP server...");
49
+ createMcpServer(server, ops, resources, prompts, {
50
+ logger,
51
+ toolCtx: () => baseCtx,
52
+ promptCtx: () => ({ locale: "en" }),
53
+ resourceCtx: () => ({ locale: "en" }),
54
+ presentations: new PresentationRegistry(presentations)
55
+ });
56
+ const transport = new WebStandardStreamableHTTPServerTransport({
57
+ sessionIdGenerator: stateful ? () => randomUUID() : undefined,
58
+ enableJsonResponse: true
59
+ });
60
+ return server.connect(transport).then(() => ({ server, transport }));
61
+ }
62
+ async function closeSessionState(state) {
63
+ await Promise.allSettled([state.transport.close(), state.server.close()]);
64
+ }
65
+ function toErrorMessage(error) {
66
+ return error instanceof Error ? error.stack ?? error.message : String(error);
67
+ }
68
+ function createMcpElysiaHandler({
69
+ logger,
70
+ path,
71
+ serverName,
72
+ ops,
73
+ resources,
74
+ prompts,
75
+ presentations
76
+ }) {
77
+ logger.info("Setting up MCP handler...");
78
+ const isStateful = process.env.CONTRACTSPEC_MCP_STATEFUL === "1";
79
+ const sessions = new Map;
80
+ async function handleStateless(request) {
81
+ const state = await createSessionState({
82
+ logger,
83
+ path,
84
+ serverName,
85
+ ops,
86
+ resources,
87
+ prompts,
88
+ presentations,
89
+ stateful: false
90
+ });
91
+ try {
92
+ return await state.transport.handleRequest(request);
93
+ } finally {
94
+ await closeSessionState(state);
95
+ }
96
+ }
97
+ async function closeSession(sessionId) {
98
+ const state = sessions.get(sessionId);
99
+ if (!state)
100
+ return;
101
+ sessions.delete(sessionId);
102
+ await closeSessionState(state);
103
+ }
104
+ async function handleStateful(request) {
105
+ const requestedSessionId = request.headers.get("mcp-session-id");
106
+ let state;
107
+ let createdState = false;
108
+ if (requestedSessionId) {
109
+ const existing = sessions.get(requestedSessionId);
110
+ if (!existing) {
111
+ return createJsonRpcErrorResponse(404, -32001, "Session not found");
112
+ }
113
+ state = existing;
114
+ } else {
115
+ state = await createSessionState({
64
116
  logger,
65
- toolCtx: () => baseCtx,
66
- promptCtx: () => ({ locale: "en" }),
67
- resourceCtx: () => ({ locale: "en" }),
68
- presentations: new PresentationRegistry(presentations)
117
+ path,
118
+ serverName,
119
+ ops,
120
+ resources,
121
+ prompts,
122
+ presentations,
123
+ stateful: true
124
+ });
125
+ createdState = true;
126
+ }
127
+ try {
128
+ const response = await state.transport.handleRequest(request);
129
+ const activeSessionId = state.transport.sessionId;
130
+ if (activeSessionId && !sessions.has(activeSessionId)) {
131
+ sessions.set(activeSessionId, state);
132
+ }
133
+ if (request.method === "DELETE" && activeSessionId) {
134
+ await closeSession(activeSessionId);
135
+ } else if (!activeSessionId && createdState) {
136
+ await closeSessionState(state);
137
+ }
138
+ return response;
139
+ } catch (error) {
140
+ if (createdState) {
141
+ await closeSessionState(state);
142
+ }
143
+ throw error;
144
+ }
145
+ }
146
+ return new Elysia({ name: `mcp-${serverName}` }).all(path, async ({ request }) => {
147
+ try {
148
+ if (isStateful) {
149
+ return await handleStateful(request);
150
+ }
151
+ return await handleStateless(request);
152
+ } catch (error) {
153
+ logger.error("Error handling MCP request", {
154
+ path,
155
+ method: request.method,
156
+ error: toErrorMessage(error)
69
157
  });
158
+ return createJsonRpcErrorResponse(500, -32000, "Internal error");
70
159
  }
71
160
  });
72
161
  }