@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.
@@ -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
+ }