@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.
- package/.turbo/turbo-build.log +14 -14
- package/CHANGELOG.md +51 -0
- package/dist/application/index.js +138 -49
- package/dist/application/mcp/cliMcp.d.ts +3 -56
- package/dist/application/mcp/cliMcp.js +138 -49
- package/dist/application/mcp/common.d.ts +5 -57
- package/dist/application/mcp/common.js +138 -49
- package/dist/application/mcp/docsMcp.d.ts +3 -56
- package/dist/application/mcp/docsMcp.js +138 -49
- package/dist/application/mcp/index.js +138 -49
- package/dist/application/mcp/internalMcp.d.ts +3 -56
- package/dist/application/mcp/internalMcp.js +138 -49
- package/dist/node/application/index.js +138 -49
- package/dist/node/application/mcp/cliMcp.js +138 -49
- package/dist/node/application/mcp/common.js +138 -49
- package/dist/node/application/mcp/docsMcp.js +138 -49
- package/dist/node/application/mcp/index.js +138 -49
- package/dist/node/application/mcp/internalMcp.js +138 -49
- package/package.json +17 -17
- package/src/application/mcp/common.ts +180 -87
|
@@ -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 {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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 {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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 {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contractspec/bundle.library",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"clean": "rm -rf dist",
|
|
@@ -2053,17 +2053,17 @@
|
|
|
2053
2053
|
},
|
|
2054
2054
|
"dependencies": {
|
|
2055
2055
|
"@apollo/client": "^4.1.4",
|
|
2056
|
-
"@contractspec/lib.contracts-spec": "2.
|
|
2057
|
-
"@contractspec/lib.contracts-library": "2.
|
|
2058
|
-
"@contractspec/lib.contracts-runtime-server-mcp": "2.
|
|
2059
|
-
"@contractspec/lib.design-system": "2.
|
|
2060
|
-
"@contractspec/lib.example-shared-ui": "2.
|
|
2061
|
-
"@contractspec/lib.logger": "2.
|
|
2062
|
-
"@contractspec/lib.runtime-sandbox": "1.
|
|
2063
|
-
"@contractspec/lib.schema": "2.
|
|
2064
|
-
"@contractspec/lib.ui-kit-web": "2.
|
|
2065
|
-
"@contractspec/lib.ui-link": "2.
|
|
2066
|
-
"@contractspec/module.examples": "2.
|
|
2056
|
+
"@contractspec/lib.contracts-spec": "2.6.0",
|
|
2057
|
+
"@contractspec/lib.contracts-library": "2.6.0",
|
|
2058
|
+
"@contractspec/lib.contracts-runtime-server-mcp": "2.6.0",
|
|
2059
|
+
"@contractspec/lib.design-system": "2.6.0",
|
|
2060
|
+
"@contractspec/lib.example-shared-ui": "2.6.0",
|
|
2061
|
+
"@contractspec/lib.logger": "2.6.0",
|
|
2062
|
+
"@contractspec/lib.runtime-sandbox": "1.6.0",
|
|
2063
|
+
"@contractspec/lib.schema": "2.6.0",
|
|
2064
|
+
"@contractspec/lib.ui-kit-web": "2.6.0",
|
|
2065
|
+
"@contractspec/lib.ui-link": "2.6.0",
|
|
2066
|
+
"@contractspec/module.examples": "2.6.0",
|
|
2067
2067
|
"@dnd-kit/core": "^6.1.0",
|
|
2068
2068
|
"@dnd-kit/sortable": "^10.0.0",
|
|
2069
2069
|
"@dnd-kit/utilities": "^3.2.2",
|
|
@@ -2079,16 +2079,16 @@
|
|
|
2079
2079
|
"posthog-react-native": "^4.34.0",
|
|
2080
2080
|
"react-hook-form": "^7.70.0",
|
|
2081
2081
|
"zod": "^4.3.5",
|
|
2082
|
-
"@contractspec/lib.contracts-integrations": "2.
|
|
2083
|
-
"@contractspec/lib.contracts-runtime-server-rest": "2.
|
|
2084
|
-
"@contractspec/lib.contracts-runtime-server-graphql": "2.
|
|
2082
|
+
"@contractspec/lib.contracts-integrations": "2.6.0",
|
|
2083
|
+
"@contractspec/lib.contracts-runtime-server-rest": "2.6.0",
|
|
2084
|
+
"@contractspec/lib.contracts-runtime-server-graphql": "2.6.0"
|
|
2085
2085
|
},
|
|
2086
2086
|
"devDependencies": {
|
|
2087
2087
|
"@types/react": "~19.2.14",
|
|
2088
|
-
"@contractspec/tool.typescript": "2.
|
|
2088
|
+
"@contractspec/tool.typescript": "2.6.0",
|
|
2089
2089
|
"typescript": "^5.9.3",
|
|
2090
2090
|
"@types/bun": "~1.3.9",
|
|
2091
|
-
"@contractspec/tool.bun": "2.
|
|
2091
|
+
"@contractspec/tool.bun": "2.6.0"
|
|
2092
2092
|
},
|
|
2093
2093
|
"publishConfig": {
|
|
2094
2094
|
"access": "public",
|