@mcp-layer/test-server 0.0.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/LICENSE +21 -0
- package/README.md +138 -0
- package/package.json +40 -0
- package/src/bin.js +14 -0
- package/src/config.js +12 -0
- package/src/data/manual.js +22 -0
- package/src/data/notes.js +72 -0
- package/src/data/references.js +20 -0
- package/src/http.js +6 -0
- package/src/index.js +54 -0
- package/src/prompts/index.js +9 -0
- package/src/prompts/welcome.js +63 -0
- package/src/resources/index.js +14 -0
- package/src/resources/manual.js +43 -0
- package/src/resources/notes.js +104 -0
- package/src/resources/ui.js +58 -0
- package/src/shared/capabilities.js +18 -0
- package/src/subscriptions/resources.js +70 -0
- package/src/tools/add.js +44 -0
- package/src/tools/annotated.js +44 -0
- package/src/tools/batch.js +43 -0
- package/src/tools/booking.js +107 -0
- package/src/tools/dashboard.js +53 -0
- package/src/tools/echo.js +45 -0
- package/src/tools/files.js +79 -0
- package/src/tools/index.js +37 -0
- package/src/tools/logs.js +56 -0
- package/src/tools/note-update.js +56 -0
- package/src/tools/present.js +76 -0
- package/src/tools/progress.js +85 -0
- package/src/tools/rebalance.js +49 -0
- package/src/tools/roots.js +98 -0
- package/src/tools/summaries.js +96 -0
- package/src/transport/http.js +77 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Register a tool that mutates note resources and emits update notifications.
|
|
5
|
+
* @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
|
|
6
|
+
* @param {{ notes: Map<string, Record<string, string>> }} context
|
|
7
|
+
* @param {{ notifyResourceUpdated: (uri: string) => Promise<void> }} notifier
|
|
8
|
+
*/
|
|
9
|
+
export function registerNoteUpdate(server, context, notifier) {
|
|
10
|
+
const inputSchema = {
|
|
11
|
+
topic: z.string().min(1),
|
|
12
|
+
detail: z.string().min(1).default('summary'),
|
|
13
|
+
text: z.string().min(1)
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Update the referenced note and broadcast notifications/resources/updated.
|
|
18
|
+
* @param {{ topic: string, detail: string, text: string }} args
|
|
19
|
+
* @returns {Promise<import('@modelcontextprotocol/sdk/types.js').CallToolResult>}
|
|
20
|
+
*/
|
|
21
|
+
async function updateNoteTool(args) {
|
|
22
|
+
const topic = args.topic.toLowerCase();
|
|
23
|
+
const entry = context.notes.get(topic) ?? {};
|
|
24
|
+
entry[args.detail] = args.text;
|
|
25
|
+
context.notes.set(topic, entry);
|
|
26
|
+
const uri = `note://${topic}/${args.detail}`;
|
|
27
|
+
await notifier.notifyResourceUpdated(uri);
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
content: [
|
|
31
|
+
{
|
|
32
|
+
type: 'text',
|
|
33
|
+
text: `Updated ${uri}.`
|
|
34
|
+
}
|
|
35
|
+
],
|
|
36
|
+
structuredContent: {
|
|
37
|
+
uri,
|
|
38
|
+
text: args.text
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
server.registerTool(
|
|
44
|
+
'note-update',
|
|
45
|
+
{
|
|
46
|
+
title: 'Note Update',
|
|
47
|
+
description: 'Mutate note:// resources and trigger resources/updated notifications.',
|
|
48
|
+
inputSchema,
|
|
49
|
+
outputSchema: {
|
|
50
|
+
uri: z.string(),
|
|
51
|
+
text: z.string()
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
updateNoteTool
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
const PNG_BASE64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO3Z5wAAAABJRU5ErkJggg==';
|
|
4
|
+
const WAV_BASE64 = 'UklGRiQAAABXQVZFZm10IBAAAAABAAEAESsAACJWAAACABAAZGF0YQAAAAA=';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Register the present tool to emit mixed content types.
|
|
8
|
+
* @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
|
|
9
|
+
* @returns {ReturnType<import('@modelcontextprotocol/sdk/server/mcp.js').McpServer['registerTool']>}
|
|
10
|
+
*/
|
|
11
|
+
export function registerPresent(server) {
|
|
12
|
+
const inputSchema = {
|
|
13
|
+
title: z.string().min(1, 'Title must be provided')
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Emit markdown, image, audio, and resource link content items.
|
|
18
|
+
* @param {{ title: string }} args
|
|
19
|
+
* @returns {Promise<import('@modelcontextprotocol/sdk/types.js').CallToolResult>}
|
|
20
|
+
*/
|
|
21
|
+
async function presentTool(args) {
|
|
22
|
+
const markdown = `# ${args.title}\n\n- Alpha\n- Beta\n\n\`\`\`json\n{\"ok\":true}\n\`\`\``;
|
|
23
|
+
return {
|
|
24
|
+
content: [
|
|
25
|
+
{
|
|
26
|
+
type: 'text',
|
|
27
|
+
text: markdown,
|
|
28
|
+
mimeType: 'text/markdown'
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
type: 'image',
|
|
32
|
+
mimeType: 'image/png',
|
|
33
|
+
data: PNG_BASE64
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
type: 'audio',
|
|
37
|
+
mimeType: 'audio/wav',
|
|
38
|
+
data: WAV_BASE64
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
type: 'resource_link',
|
|
42
|
+
uri: 'resource://manual',
|
|
43
|
+
name: 'Server Manual',
|
|
44
|
+
description: 'Markdown manual for the test server.',
|
|
45
|
+
mimeType: 'text/markdown'
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
type: 'resource',
|
|
49
|
+
resource: {
|
|
50
|
+
uri: 'resource://manual',
|
|
51
|
+
mimeType: 'text/markdown',
|
|
52
|
+
text: '# Embedded Manual\n\nUse resources/read for the full manual.'
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
],
|
|
56
|
+
structuredContent: {
|
|
57
|
+
title: args.title,
|
|
58
|
+
types: ['text', 'image', 'audio', 'resource_link', 'resource']
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return server.registerTool(
|
|
64
|
+
'present',
|
|
65
|
+
{
|
|
66
|
+
title: 'Present',
|
|
67
|
+
description: 'Return mixed content types for CLI formatting.',
|
|
68
|
+
inputSchema,
|
|
69
|
+
outputSchema: {
|
|
70
|
+
title: z.string(),
|
|
71
|
+
types: z.array(z.string())
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
presentTool
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Register the progress tool that emits notifications/progress updates.
|
|
5
|
+
* @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
|
|
6
|
+
*/
|
|
7
|
+
export function registerProgress(server) {
|
|
8
|
+
const inputSchema = {
|
|
9
|
+
steps: z.number().int().min(1).max(5).default(3),
|
|
10
|
+
delayMs: z.number().int().min(1).max(200).default(10)
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Wait between steps and emit progress notifications when requested.
|
|
15
|
+
* @param {{ steps: number, delayMs: number }} args
|
|
16
|
+
* @param {{ _meta?: { progressToken?: string | number }, signal: AbortSignal, sendNotification: (notification: any) => Promise<void> }} extra
|
|
17
|
+
* @returns {Promise<import('@modelcontextprotocol/sdk/types.js').CallToolResult>}
|
|
18
|
+
*/
|
|
19
|
+
async function progressTool(args, extra) {
|
|
20
|
+
const token = extra?._meta?.progressToken;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Sleep helper respecting cancellation.
|
|
24
|
+
* @returns {Promise<void>}
|
|
25
|
+
*/
|
|
26
|
+
function sleep() {
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const timer = setTimeout(resolve, args.delayMs);
|
|
29
|
+
extra.signal.addEventListener(
|
|
30
|
+
'abort',
|
|
31
|
+
() => {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
reject(new Error('progress tool aborted'));
|
|
34
|
+
},
|
|
35
|
+
{ once: true }
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (let index = 1; index <= args.steps; index += 1) {
|
|
41
|
+
if (extra.signal.aborted) {
|
|
42
|
+
throw new Error('progress tool aborted');
|
|
43
|
+
}
|
|
44
|
+
await sleep();
|
|
45
|
+
if (token) {
|
|
46
|
+
await extra.sendNotification({
|
|
47
|
+
method: 'notifications/progress',
|
|
48
|
+
params: {
|
|
49
|
+
progressToken: token,
|
|
50
|
+
progress: index,
|
|
51
|
+
total: args.steps,
|
|
52
|
+
message: `step ${index} of ${args.steps}`
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
content: [
|
|
60
|
+
{
|
|
61
|
+
type: 'text',
|
|
62
|
+
text: `Completed ${args.steps} steps.`
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
structuredContent: {
|
|
66
|
+
steps: args.steps,
|
|
67
|
+
sentProgress: Boolean(token)
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
server.registerTool(
|
|
73
|
+
'progress',
|
|
74
|
+
{
|
|
75
|
+
title: 'Progress',
|
|
76
|
+
description: 'Emit notifications/progress updates for long-running work.',
|
|
77
|
+
inputSchema,
|
|
78
|
+
outputSchema: {
|
|
79
|
+
steps: z.number().int().min(1),
|
|
80
|
+
sentProgress: z.boolean()
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
progressTool
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Register the rebalance tool that toggles core tool registrations to exercise debounced notifications.
|
|
5
|
+
* @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
|
|
6
|
+
* @param {{ echo: ReturnType<import('@modelcontextprotocol/sdk/server/mcp.js').McpServer['registerTool']>, add: ReturnType<import('@modelcontextprotocol/sdk/server/mcp.js').McpServer['registerTool']> }} handles
|
|
7
|
+
*/
|
|
8
|
+
export function registerRebalance(server, handles) {
|
|
9
|
+
const inputSchema = {
|
|
10
|
+
cycles: z.number().int().min(1).max(5).default(2)
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Toggle core tools repeatedly to demonstrate notification debouncing.
|
|
15
|
+
* @param {{ cycles: number }} args
|
|
16
|
+
* @returns {Promise<import('@modelcontextprotocol/sdk/types.js').CallToolResult>}
|
|
17
|
+
*/
|
|
18
|
+
async function rebalanceTool(args) {
|
|
19
|
+
for (let index = 0; index < args.cycles; index += 1) {
|
|
20
|
+
handles.echo.disable();
|
|
21
|
+
handles.add.disable();
|
|
22
|
+
handles.echo.enable();
|
|
23
|
+
handles.add.enable();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
content: [
|
|
28
|
+
{
|
|
29
|
+
type: 'text',
|
|
30
|
+
text: `Rebalanced core tools for ${args.cycles} cycles.`
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
structuredContent: { cycles: args.cycles }
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
server.registerTool(
|
|
38
|
+
'rebalance',
|
|
39
|
+
{
|
|
40
|
+
title: 'Rebalance',
|
|
41
|
+
description: 'Batch enable/disable operations to test debounced notifications.',
|
|
42
|
+
inputSchema,
|
|
43
|
+
outputSchema: {
|
|
44
|
+
cycles: z.number().int().min(1)
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
rebalanceTool
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Register the roots tool that calls roots/list on capable clients.
|
|
5
|
+
* @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
|
|
6
|
+
* @param {{ hasCapability: (capability: 'roots') => boolean }} capabilities
|
|
7
|
+
*/
|
|
8
|
+
export function registerRoots(server, capabilities) {
|
|
9
|
+
const inputSchema = {
|
|
10
|
+
includeMeta: z.boolean().default(false)
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Request roots/list from the client to expose workspace locations.
|
|
15
|
+
* @param {{ includeMeta: boolean }} args
|
|
16
|
+
* @returns {Promise<import('@modelcontextprotocol/sdk/types.js').CallToolResult>}
|
|
17
|
+
*/
|
|
18
|
+
async function rootsTool(args) {
|
|
19
|
+
if (!capabilities.hasCapability('roots')) {
|
|
20
|
+
const notice = 'Connected client did not declare roots capability.';
|
|
21
|
+
return {
|
|
22
|
+
content: [
|
|
23
|
+
{
|
|
24
|
+
type: 'text',
|
|
25
|
+
text: notice
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
structuredContent: {
|
|
29
|
+
count: 0,
|
|
30
|
+
roots: [],
|
|
31
|
+
usedCapability: false
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const result = await server.server.listRoots({});
|
|
38
|
+
const roots = result.roots.map(function mapRoots(entry) {
|
|
39
|
+
return {
|
|
40
|
+
uri: entry.uri,
|
|
41
|
+
name: entry.name ?? null,
|
|
42
|
+
meta: args.includeMeta ? entry._meta ?? null : null
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
content: [
|
|
47
|
+
{
|
|
48
|
+
type: 'text',
|
|
49
|
+
text: `Received ${roots.length} roots from client.`
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
structuredContent: {
|
|
53
|
+
count: roots.length,
|
|
54
|
+
roots,
|
|
55
|
+
usedCapability: true
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
60
|
+
return {
|
|
61
|
+
content: [
|
|
62
|
+
{
|
|
63
|
+
type: 'text',
|
|
64
|
+
text: message
|
|
65
|
+
}
|
|
66
|
+
],
|
|
67
|
+
structuredContent: {
|
|
68
|
+
count: 0,
|
|
69
|
+
roots: [],
|
|
70
|
+
usedCapability: true,
|
|
71
|
+
error: message
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
server.registerTool(
|
|
78
|
+
'roots',
|
|
79
|
+
{
|
|
80
|
+
title: 'Roots',
|
|
81
|
+
description: 'Call roots/list on capable clients.',
|
|
82
|
+
inputSchema,
|
|
83
|
+
outputSchema: {
|
|
84
|
+
count: z.number().int(),
|
|
85
|
+
roots: z.array(
|
|
86
|
+
z.object({
|
|
87
|
+
uri: z.string(),
|
|
88
|
+
name: z.string().nullable(),
|
|
89
|
+
meta: z.record(z.unknown()).nullable().optional()
|
|
90
|
+
})
|
|
91
|
+
),
|
|
92
|
+
usedCapability: z.boolean(),
|
|
93
|
+
error: z.string().optional()
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
rootsTool
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Register the summaries tool that delegates to sampling/createMessage.
|
|
5
|
+
* @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
|
|
6
|
+
* @param {{ hasCapability: (capability: 'sampling') => boolean }} capabilities
|
|
7
|
+
*/
|
|
8
|
+
export function registerSummaries(server, capabilities) {
|
|
9
|
+
const inputSchema = {
|
|
10
|
+
text: z.string().min(1, 'Text is required'),
|
|
11
|
+
maxTokens: z.number().int().min(64).max(512).default(200)
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Request a summary via sampling/createMessage when supported.
|
|
16
|
+
* @param {{ text: string, maxTokens: number }} args
|
|
17
|
+
* @returns {Promise<import('@modelcontextprotocol/sdk/types.js').CallToolResult>}
|
|
18
|
+
*/
|
|
19
|
+
async function summaryTool(args) {
|
|
20
|
+
if (!capabilities.hasCapability('sampling')) {
|
|
21
|
+
const notice = 'Connected client did not declare sampling capability.';
|
|
22
|
+
return {
|
|
23
|
+
content: [
|
|
24
|
+
{
|
|
25
|
+
type: 'text',
|
|
26
|
+
text: notice
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
structuredContent: {
|
|
30
|
+
summary: notice,
|
|
31
|
+
usedSampling: false,
|
|
32
|
+
model: null
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const response = await server.server.createMessage({
|
|
39
|
+
messages: [
|
|
40
|
+
{
|
|
41
|
+
role: 'user',
|
|
42
|
+
content: {
|
|
43
|
+
type: 'text',
|
|
44
|
+
text: `Summarize the following text in one sentence.\n\n${args.text}`
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
maxTokens: args.maxTokens
|
|
49
|
+
});
|
|
50
|
+
const summaryText = response.content.type === 'text' ? response.content.text : 'Sampling produced non-text output.';
|
|
51
|
+
return {
|
|
52
|
+
content: [
|
|
53
|
+
{
|
|
54
|
+
type: 'text',
|
|
55
|
+
text: summaryText
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
structuredContent: {
|
|
59
|
+
summary: summaryText,
|
|
60
|
+
usedSampling: true,
|
|
61
|
+
model: response.model
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
} catch (error) {
|
|
65
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
66
|
+
return {
|
|
67
|
+
content: [
|
|
68
|
+
{
|
|
69
|
+
type: 'text',
|
|
70
|
+
text: message
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
structuredContent: {
|
|
74
|
+
summary: message,
|
|
75
|
+
usedSampling: false,
|
|
76
|
+
model: null
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
server.registerTool(
|
|
83
|
+
'summaries',
|
|
84
|
+
{
|
|
85
|
+
title: 'Summaries',
|
|
86
|
+
description: 'Proxy sampling/createMessage to summarize text.',
|
|
87
|
+
inputSchema,
|
|
88
|
+
outputSchema: {
|
|
89
|
+
summary: z.string(),
|
|
90
|
+
usedSampling: z.boolean(),
|
|
91
|
+
model: z.string().nullable()
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
summaryTool
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
3
|
+
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
4
|
+
import { build } from '../index.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Start an HTTP server that exposes Streamable HTTP and SSE transports.
|
|
8
|
+
* @param {{ port?: number }} [opts]
|
|
9
|
+
* @returns {Promise<{ close: () => Promise<void> }>}
|
|
10
|
+
*/
|
|
11
|
+
export async function startHttpServer(opts = {}) {
|
|
12
|
+
const port = opts.port ?? Number(process.env.PORT ?? 3000);
|
|
13
|
+
const app = express();
|
|
14
|
+
app.use(express.json({ limit: '1mb' }));
|
|
15
|
+
|
|
16
|
+
/** @type {Record<string, { transport: StreamableHTTPServerTransport, server: import('@modelcontextprotocol/sdk/server/mcp.js').McpServer }>} */
|
|
17
|
+
const httpSessions = {};
|
|
18
|
+
/** @type {Record<string, { transport: SSEServerTransport, server: import('@modelcontextprotocol/sdk/server/mcp.js').McpServer }>} */
|
|
19
|
+
const sseSessions = {};
|
|
20
|
+
|
|
21
|
+
function newSession() {
|
|
22
|
+
const transport = new StreamableHTTPServerTransport({
|
|
23
|
+
enableJsonResponse: true
|
|
24
|
+
});
|
|
25
|
+
const server = build();
|
|
26
|
+
return { transport, server };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
app.post('/mcp', async (req, res) => {
|
|
30
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
31
|
+
let session = sessionId ? httpSessions[sessionId] : undefined;
|
|
32
|
+
|
|
33
|
+
if (!session) {
|
|
34
|
+
session = newSession();
|
|
35
|
+
httpSessions[session.transport.sessionId] = session;
|
|
36
|
+
res.setHeader('Mcp-Session-Id', session.transport.sessionId);
|
|
37
|
+
res.on('close', () => {
|
|
38
|
+
void session.transport.close();
|
|
39
|
+
delete httpSessions[session.transport.sessionId];
|
|
40
|
+
});
|
|
41
|
+
await session.server.connect(session.transport);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
await session.transport.handleRequest(req, res, req.body);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
app.get('/sse', async (req, res) => {
|
|
48
|
+
const transport = new SSEServerTransport('/sse/messages', res);
|
|
49
|
+
const server = build();
|
|
50
|
+
sseSessions[transport.sessionId] = { transport, server };
|
|
51
|
+
res.on('close', () => {
|
|
52
|
+
void transport.close();
|
|
53
|
+
delete sseSessions[transport.sessionId];
|
|
54
|
+
});
|
|
55
|
+
await server.connect(transport);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
app.post('/sse/messages', async (req, res) => {
|
|
59
|
+
const sessionId = req.query.sessionId;
|
|
60
|
+
const session = typeof sessionId === 'string' ? sseSessions[sessionId] : undefined;
|
|
61
|
+
if (!session) {
|
|
62
|
+
res.status(400).send('No SSE session found');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
await session.transport.handlePostMessage(req, res, req.body);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return new Promise(resolve => {
|
|
69
|
+
const listener = app.listen(port, () => {
|
|
70
|
+
resolve({
|
|
71
|
+
async close() {
|
|
72
|
+
await new Promise(r => listener.close(r));
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|