mirror of
https://github.com/modelcontextprotocol/servers.git
synced 2026-04-17 23:53:24 +02:00
Merge remote-tracking branch 'upstream/main' into add-sequentialthinking-tests-update-sdk
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ClientCapabilities,
|
||||
CompleteRequestSchema,
|
||||
CreateMessageRequest,
|
||||
CreateMessageResultSchema,
|
||||
ElicitRequest,
|
||||
ElicitResultSchema,
|
||||
GetPromptRequestSchema,
|
||||
ListPromptsRequestSchema,
|
||||
ListResourcesRequestSchema,
|
||||
@@ -14,18 +17,20 @@ import {
|
||||
ReadResourceRequestSchema,
|
||||
Resource,
|
||||
RootsListChangedNotificationSchema,
|
||||
SetLevelRequestSchema,
|
||||
ServerNotification,
|
||||
ServerRequest,
|
||||
SubscribeRequestSchema,
|
||||
Tool,
|
||||
ToolSchema,
|
||||
UnsubscribeRequestSchema,
|
||||
type Root,
|
||||
type Root
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { z } from "zod";
|
||||
import { zodToJsonSchema } from "zod-to-json-schema";
|
||||
import { readFileSync } from "fs";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname, join } from "path";
|
||||
import JSZip from "jszip";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
@@ -37,6 +42,8 @@ type ToolInput = z.infer<typeof ToolInputSchema>;
|
||||
const ToolOutputSchema = ToolSchema.shape.outputSchema;
|
||||
type ToolOutput = z.infer<typeof ToolOutputSchema>;
|
||||
|
||||
type SendRequest = RequestHandlerExtra<ServerRequest, ServerNotification>["sendRequest"];
|
||||
|
||||
/* Input schemas for tools implemented in this server */
|
||||
const EchoSchema = z.object({
|
||||
message: z.string().describe("Message to echo"),
|
||||
@@ -123,6 +130,10 @@ const StructuredContentSchema = {
|
||||
})
|
||||
};
|
||||
|
||||
const ZipResourcesInputSchema = z.object({
|
||||
files: z.record(z.string().url().describe("URL of the file to include in the zip")).describe("Mapping of file names to URLs to include in the zip"),
|
||||
});
|
||||
|
||||
enum ToolName {
|
||||
ECHO = "echo",
|
||||
ADD = "add",
|
||||
@@ -135,6 +146,7 @@ enum ToolName {
|
||||
ELICITATION = "startElicitation",
|
||||
GET_RESOURCE_LINKS = "getResourceLinks",
|
||||
STRUCTURED_CONTENT = "structuredContent",
|
||||
ZIP_RESOURCES = "zip",
|
||||
LIST_ROOTS = "listRoots"
|
||||
}
|
||||
|
||||
@@ -174,7 +186,6 @@ export const createServer = () => {
|
||||
let subsUpdateInterval: NodeJS.Timeout | undefined;
|
||||
let stdErrUpdateInterval: NodeJS.Timeout | undefined;
|
||||
|
||||
let logLevel: LoggingLevel = "debug";
|
||||
let logsUpdateInterval: NodeJS.Timeout | undefined;
|
||||
// Store client capabilities
|
||||
let clientCapabilities: ClientCapabilities | undefined;
|
||||
@@ -182,55 +193,48 @@ export const createServer = () => {
|
||||
// Roots state management
|
||||
let currentRoots: Root[] = [];
|
||||
let clientSupportsRoots = false;
|
||||
const messages = [
|
||||
{ level: "debug", data: "Debug-level message" },
|
||||
{ level: "info", data: "Info-level message" },
|
||||
{ level: "notice", data: "Notice-level message" },
|
||||
{ level: "warning", data: "Warning-level message" },
|
||||
{ level: "error", data: "Error-level message" },
|
||||
{ level: "critical", data: "Critical-level message" },
|
||||
{ level: "alert", data: "Alert level-message" },
|
||||
{ level: "emergency", data: "Emergency-level message" },
|
||||
];
|
||||
let sessionId: string | undefined;
|
||||
|
||||
const isMessageIgnored = (level: LoggingLevel): boolean => {
|
||||
const currentLevel = messages.findIndex((msg) => logLevel === msg.level);
|
||||
const messageLevel = messages.findIndex((msg) => level === msg.level);
|
||||
return messageLevel < currentLevel;
|
||||
};
|
||||
// Function to start notification intervals when a client connects
|
||||
const startNotificationIntervals = (sid?: string|undefined) => {
|
||||
sessionId = sid;
|
||||
if (!subsUpdateInterval) {
|
||||
subsUpdateInterval = setInterval(() => {
|
||||
for (const uri of subscriptions) {
|
||||
server.notification({
|
||||
method: "notifications/resources/updated",
|
||||
params: { uri },
|
||||
});
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
// Function to start notification intervals when a client connects
|
||||
const startNotificationIntervals = () => {
|
||||
if (!subsUpdateInterval) {
|
||||
subsUpdateInterval = setInterval(() => {
|
||||
for (const uri of subscriptions) {
|
||||
server.notification({
|
||||
method: "notifications/resources/updated",
|
||||
params: { uri },
|
||||
});
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
const maybeAppendSessionId = sessionId ? ` - SessionId ${sessionId}`: "";
|
||||
const messages: { level: LoggingLevel; data: string }[] = [
|
||||
{ level: "debug", data: `Debug-level message${maybeAppendSessionId}` },
|
||||
{ level: "info", data: `Info-level message${maybeAppendSessionId}` },
|
||||
{ level: "notice", data: `Notice-level message${maybeAppendSessionId}` },
|
||||
{ level: "warning", data: `Warning-level message${maybeAppendSessionId}` },
|
||||
{ level: "error", data: `Error-level message${maybeAppendSessionId}` },
|
||||
{ level: "critical", data: `Critical-level message${maybeAppendSessionId}` },
|
||||
{ level: "alert", data: `Alert level-message${maybeAppendSessionId}` },
|
||||
{ level: "emergency", data: `Emergency-level message${maybeAppendSessionId}` },
|
||||
];
|
||||
|
||||
if (!logsUpdateInterval) {
|
||||
logsUpdateInterval = setInterval(() => {
|
||||
let message = {
|
||||
method: "notifications/message",
|
||||
params: messages[Math.floor(Math.random() * messages.length)],
|
||||
};
|
||||
if (!isMessageIgnored(message.params.level as LoggingLevel))
|
||||
server.notification(message);
|
||||
}, 20000);
|
||||
if (!logsUpdateInterval) {
|
||||
console.error("Starting logs update interval");
|
||||
logsUpdateInterval = setInterval(async () => {
|
||||
await server.sendLoggingMessage( messages[Math.floor(Math.random() * messages.length)], sessionId);
|
||||
}, 15000);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Helper method to request sampling from client
|
||||
const requestSampling = async (
|
||||
context: string,
|
||||
uri: string,
|
||||
maxTokens: number = 100
|
||||
maxTokens: number = 100,
|
||||
sendRequest: SendRequest
|
||||
) => {
|
||||
const request: CreateMessageRequest = {
|
||||
method: "sampling/createMessage",
|
||||
@@ -251,22 +255,24 @@ export const createServer = () => {
|
||||
},
|
||||
};
|
||||
|
||||
return await server.request(request, CreateMessageResultSchema);
|
||||
return await sendRequest(request, CreateMessageResultSchema);
|
||||
|
||||
};
|
||||
|
||||
const requestElicitation = async (
|
||||
message: string,
|
||||
requestedSchema: any
|
||||
requestedSchema: any,
|
||||
sendRequest: SendRequest
|
||||
) => {
|
||||
const request = {
|
||||
const request: ElicitRequest = {
|
||||
method: 'elicitation/create',
|
||||
params: {
|
||||
message,
|
||||
requestedSchema
|
||||
}
|
||||
requestedSchema,
|
||||
},
|
||||
};
|
||||
|
||||
return await server.request(request, z.any());
|
||||
return await sendRequest(request, ElicitResultSchema);
|
||||
};
|
||||
|
||||
const ALL_RESOURCES: Resource[] = Array.from({ length: 100 }, (_, i) => {
|
||||
@@ -344,12 +350,9 @@ export const createServer = () => {
|
||||
throw new Error(`Unknown resource: ${uri}`);
|
||||
});
|
||||
|
||||
server.setRequestHandler(SubscribeRequestSchema, async (request) => {
|
||||
server.setRequestHandler(SubscribeRequestSchema, async (request, extra) => {
|
||||
const { uri } = request.params;
|
||||
subscriptions.add(uri);
|
||||
|
||||
// Request sampling from client when someone subscribes
|
||||
await requestSampling("A new subscription was started", uri);
|
||||
return {};
|
||||
});
|
||||
|
||||
@@ -535,6 +538,11 @@ export const createServer = () => {
|
||||
inputSchema: zodToJsonSchema(StructuredContentSchema.input) as ToolInput,
|
||||
outputSchema: zodToJsonSchema(StructuredContentSchema.output) as ToolOutput,
|
||||
},
|
||||
{
|
||||
name: ToolName.ZIP_RESOURCES,
|
||||
description: "Compresses the provided resource files (mapping of name to URI, which can be a data URI) to a zip file, which it returns as a data URI resource link.",
|
||||
inputSchema: zodToJsonSchema(ZipResourcesInputSchema) as ToolInput,
|
||||
}
|
||||
];
|
||||
if (clientCapabilities!.roots) tools.push ({
|
||||
name: ToolName.LIST_ROOTS,
|
||||
@@ -551,7 +559,7 @@ export const createServer = () => {
|
||||
return { tools };
|
||||
});
|
||||
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request,extra) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
if (name === ToolName.ECHO) {
|
||||
@@ -593,7 +601,7 @@ export const createServer = () => {
|
||||
total: steps,
|
||||
progressToken,
|
||||
},
|
||||
});
|
||||
},{relatedRequestId: extra.requestId});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -625,7 +633,8 @@ export const createServer = () => {
|
||||
const result = await requestSampling(
|
||||
prompt,
|
||||
ToolName.SAMPLE_LLM,
|
||||
maxTokens
|
||||
maxTokens,
|
||||
extra.sendRequest
|
||||
);
|
||||
return {
|
||||
content: [
|
||||
@@ -744,14 +753,20 @@ export const createServer = () => {
|
||||
type: 'object',
|
||||
properties: {
|
||||
color: { type: 'string', description: 'Favorite color' },
|
||||
number: { type: 'integer', description: 'Favorite number', minimum: 1, maximum: 100 },
|
||||
number: {
|
||||
type: 'integer',
|
||||
description: 'Favorite number',
|
||||
minimum: 1,
|
||||
maximum: 100,
|
||||
},
|
||||
pets: {
|
||||
type: 'string',
|
||||
enum: ['cats', 'dogs', 'birds', 'fish', 'reptiles'],
|
||||
description: 'Favorite pets'
|
||||
description: 'Favorite pets',
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
extra.sendRequest
|
||||
);
|
||||
|
||||
// Handle different response actions
|
||||
@@ -840,6 +855,37 @@ export const createServer = () => {
|
||||
};
|
||||
}
|
||||
|
||||
if (name === ToolName.ZIP_RESOURCES) {
|
||||
const { files } = ZipResourcesInputSchema.parse(args);
|
||||
|
||||
const zip = new JSZip();
|
||||
|
||||
for (const [fileName, fileUrl] of Object.entries(files)) {
|
||||
try {
|
||||
const response = await fetch(fileUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch ${fileUrl}: ${response.statusText}`);
|
||||
}
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
zip.file(fileName, arrayBuffer);
|
||||
} catch (error) {
|
||||
throw new Error(`Error fetching file ${fileUrl}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
const uri = `data:application/zip;base64,${await zip.generateAsync({ type: "base64" })}`;
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "resource_link",
|
||||
mimeType: "application/zip",
|
||||
uri,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (name === ToolName.LIST_ROOTS) {
|
||||
ListRootsSchema.parse(args);
|
||||
|
||||
@@ -918,23 +964,6 @@ export const createServer = () => {
|
||||
throw new Error(`Unknown reference type`);
|
||||
});
|
||||
|
||||
server.setRequestHandler(SetLevelRequestSchema, async (request) => {
|
||||
const { level } = request.params;
|
||||
logLevel = level;
|
||||
|
||||
// Demonstrate different log levels
|
||||
await server.notification({
|
||||
method: "notifications/message",
|
||||
params: {
|
||||
level: "debug",
|
||||
logger: "test-server",
|
||||
data: `Logging level set to: ${logLevel}`,
|
||||
},
|
||||
});
|
||||
|
||||
return {};
|
||||
});
|
||||
|
||||
// Roots protocol handlers
|
||||
server.setNotificationHandler(RootsListChangedNotificationSchema, async () => {
|
||||
try {
|
||||
@@ -944,24 +973,18 @@ export const createServer = () => {
|
||||
currentRoots = response.roots;
|
||||
|
||||
// Log the roots update for demonstration
|
||||
await server.notification({
|
||||
method: "notifications/message",
|
||||
params: {
|
||||
await server.sendLoggingMessage({
|
||||
level: "info",
|
||||
logger: "everything-server",
|
||||
data: `Roots updated: ${currentRoots.length} root(s) received from client`,
|
||||
},
|
||||
});
|
||||
}, sessionId);
|
||||
}
|
||||
} catch (error) {
|
||||
await server.notification({
|
||||
method: "notifications/message",
|
||||
params: {
|
||||
await server.sendLoggingMessage({
|
||||
level: "error",
|
||||
logger: "everything-server",
|
||||
data: `Failed to request roots from client: ${error instanceof Error ? error.message : String(error)}`,
|
||||
},
|
||||
});
|
||||
}, sessionId);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -976,43 +999,31 @@ export const createServer = () => {
|
||||
if (response && 'roots' in response) {
|
||||
currentRoots = response.roots;
|
||||
|
||||
await server.notification({
|
||||
method: "notifications/message",
|
||||
params: {
|
||||
await server.sendLoggingMessage({
|
||||
level: "info",
|
||||
logger: "everything-server",
|
||||
data: `Initial roots received: ${currentRoots.length} root(s) from client`,
|
||||
},
|
||||
});
|
||||
}, sessionId);
|
||||
} else {
|
||||
await server.notification({
|
||||
method: "notifications/message",
|
||||
params: {
|
||||
await server.sendLoggingMessage({
|
||||
level: "warning",
|
||||
logger: "everything-server",
|
||||
data: "Client returned no roots set",
|
||||
},
|
||||
});
|
||||
}, sessionId);
|
||||
}
|
||||
} catch (error) {
|
||||
await server.notification({
|
||||
method: "notifications/message",
|
||||
params: {
|
||||
await server.sendLoggingMessage({
|
||||
level: "error",
|
||||
logger: "everything-server",
|
||||
data: `Failed to request initial roots from client: ${error instanceof Error ? error.message : String(error)}`,
|
||||
},
|
||||
});
|
||||
}, sessionId);
|
||||
}
|
||||
} else {
|
||||
await server.notification({
|
||||
method: "notifications/message",
|
||||
params: {
|
||||
await server.sendLoggingMessage({
|
||||
level: "info",
|
||||
logger: "everything-server",
|
||||
data: "Client does not support MCP roots protocol",
|
||||
},
|
||||
});
|
||||
}, sessionId);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -24,10 +24,12 @@
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.19.1",
|
||||
"express": "^4.21.1",
|
||||
"jszip": "^3.10.1",
|
||||
"zod": "^3.23.8",
|
||||
"zod-to-json-schema": "^3.23.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.0",
|
||||
"shx": "^0.3.4",
|
||||
"typescript": "^5.6.2"
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
||||
import express from "express";
|
||||
import { createServer } from "./everything.js";
|
||||
import cors from 'cors';
|
||||
|
||||
console.error('Starting SSE server...');
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(cors({
|
||||
"origin": "*", // use "*" with caution in production
|
||||
"methods": "GET,POST",
|
||||
"preflightContinue": false,
|
||||
"optionsSuccessStatus": 204,
|
||||
})); // Enable CORS for all routes so Inspector can connect
|
||||
const transports: Map<string, SSEServerTransport> = new Map<string, SSEServerTransport>();
|
||||
|
||||
app.get("/sse", async (req, res) => {
|
||||
@@ -26,7 +32,7 @@ app.get("/sse", async (req, res) => {
|
||||
console.error("Client Connected: ", transport.sessionId);
|
||||
|
||||
// Start notification intervals after client connects
|
||||
startNotificationIntervals();
|
||||
startNotificationIntervals(transport.sessionId);
|
||||
|
||||
// Handle close of connection
|
||||
server.onclose = async () => {
|
||||
|
||||
@@ -6,17 +6,18 @@ import { createServer } from "./everything.js";
|
||||
console.error('Starting default (STDIO) server...');
|
||||
|
||||
async function main() {
|
||||
const transport = new StdioServerTransport();
|
||||
const {server, cleanup} = createServer();
|
||||
const transport = new StdioServerTransport();
|
||||
const {server, cleanup, startNotificationIntervals} = createServer();
|
||||
|
||||
await server.connect(transport);
|
||||
await server.connect(transport);
|
||||
startNotificationIntervals();
|
||||
|
||||
// Cleanup on exit
|
||||
process.on("SIGINT", async () => {
|
||||
await cleanup();
|
||||
await server.close();
|
||||
process.exit(0);
|
||||
});
|
||||
// Cleanup on exit
|
||||
process.on("SIGINT", async () => {
|
||||
await cleanup();
|
||||
await server.close();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
|
||||
@@ -3,10 +3,22 @@ import { InMemoryEventStore } from '@modelcontextprotocol/sdk/examples/shared/in
|
||||
import express, { Request, Response } from "express";
|
||||
import { createServer } from "./everything.js";
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import cors from 'cors';
|
||||
|
||||
console.error('Starting Streamable HTTP server...');
|
||||
|
||||
const app = express();
|
||||
app.use(cors({
|
||||
"origin": "*", // use "*" with caution in production
|
||||
"methods": "GET,POST,DELETE",
|
||||
"preflightContinue": false,
|
||||
"optionsSuccessStatus": 204,
|
||||
"exposedHeaders": [
|
||||
'mcp-session-id',
|
||||
'last-event-id',
|
||||
'mcp-protocol-version'
|
||||
]
|
||||
})); // Enable CORS for all routes so Inspector can connect
|
||||
|
||||
const transports: Map<string, StreamableHTTPServerTransport> = new Map<string, StreamableHTTPServerTransport>();
|
||||
|
||||
@@ -15,6 +27,7 @@ app.post('/mcp', async (req: Request, res: Response) => {
|
||||
try {
|
||||
// Check for existing session ID
|
||||
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
||||
|
||||
let transport: StreamableHTTPServerTransport;
|
||||
|
||||
if (sessionId && transports.has(sessionId)) {
|
||||
@@ -22,7 +35,7 @@ app.post('/mcp', async (req: Request, res: Response) => {
|
||||
transport = transports.get(sessionId)!;
|
||||
} else if (!sessionId) {
|
||||
|
||||
const { server, cleanup } = createServer();
|
||||
const { server, cleanup, startNotificationIntervals } = createServer();
|
||||
|
||||
// New initialization request
|
||||
const eventStore = new InMemoryEventStore();
|
||||
@@ -53,7 +66,11 @@ app.post('/mcp', async (req: Request, res: Response) => {
|
||||
await server.connect(transport);
|
||||
|
||||
await transport.handleRequest(req, res);
|
||||
return; // Already handled
|
||||
|
||||
// Wait until initialize is complete and transport will have a sessionId
|
||||
startNotificationIntervals(transport.sessionId);
|
||||
|
||||
return; // Already handled
|
||||
} else {
|
||||
// Invalid request - no session ID or not initialization request
|
||||
res.status(400).json({
|
||||
|
||||
Reference in New Issue
Block a user