avancement planning

This commit is contained in:
2026-05-26 11:58:39 +02:00
parent 619a2b240a
commit 150b97cd2e
4892 changed files with 99214 additions and 429382 deletions
@@ -1,78 +1,93 @@
import express from 'express';
import { randomUUID } from 'node:crypto';
import { z } from 'zod';
import * as z from 'zod/v4';
import { McpServer } from '../../server/mcp.js';
import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js';
import { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter } from '../../server/auth/router.js';
import { requireBearerAuth } from '../../server/auth/middleware/bearerAuth.js';
import { isInitializeRequest } from '../../types.js';
import { createMcpExpressApp } from '../../server/express.js';
import { ElicitResultSchema, isInitializeRequest } from '../../types.js';
import { InMemoryEventStore } from '../shared/inMemoryEventStore.js';
import { InMemoryTaskStore, InMemoryTaskMessageQueue } from '../../experimental/tasks/stores/in-memory.js';
import { setupAuthServer } from './demoInMemoryOAuthProvider.js';
import { checkResourceAllowed } from 'src/shared/auth-utils.js';
import cors from 'cors';
import { checkResourceAllowed } from '../../shared/auth-utils.js';
// Check for OAuth flag
const useOAuth = process.argv.includes('--oauth');
const strictOAuth = process.argv.includes('--oauth-strict');
// Create shared task store for demonstration
const taskStore = new InMemoryTaskStore();
// Create an MCP server with implementation details
const getServer = () => {
const server = new McpServer({
name: 'simple-streamable-http-server',
version: '1.0.0'
}, { capabilities: { logging: {} } });
version: '1.0.0',
icons: [{ src: './mcp.svg', sizes: ['512x512'], mimeType: 'image/svg+xml' }],
websiteUrl: 'https://github.com/modelcontextprotocol/typescript-sdk'
}, {
capabilities: { logging: {}, tasks: { requests: { tools: { call: {} } } } },
taskStore, // Enable task support
taskMessageQueue: new InMemoryTaskMessageQueue()
});
// Register a simple tool that returns a greeting
server.registerTool('greet', {
title: 'Greeting Tool', // Display name for UI
description: 'A simple greeting tool',
inputSchema: {
name: z.string().describe('Name to greet'),
},
name: z.string().describe('Name to greet')
}
}, async ({ name }) => {
return {
content: [
{
type: 'text',
text: `Hello, ${name}!`,
},
],
text: `Hello, ${name}!`
}
]
};
});
// Register a tool that sends multiple greetings with notifications (with annotations)
server.tool('multi-greet', 'A tool that sends different greetings with delays between them', {
name: z.string().describe('Name to greet'),
}, {
title: 'Multiple Greeting Tool',
readOnlyHint: true,
openWorldHint: false
}, async ({ name }, { sendNotification }) => {
server.registerTool('multi-greet', {
description: 'A tool that sends different greetings with delays between them',
inputSchema: {
name: z.string().describe('Name to greet')
},
annotations: {
title: 'Multiple Greeting Tool',
readOnlyHint: true,
openWorldHint: false
}
}, async ({ name }, extra) => {
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
await sendNotification({
method: "notifications/message",
params: { level: "debug", data: `Starting multi-greet for ${name}` }
});
await server.sendLoggingMessage({
level: 'debug',
data: `Starting multi-greet for ${name}`
}, extra.sessionId);
await sleep(1000); // Wait 1 second before first greeting
await sendNotification({
method: "notifications/message",
params: { level: "info", data: `Sending first greeting to ${name}` }
});
await server.sendLoggingMessage({
level: 'info',
data: `Sending first greeting to ${name}`
}, extra.sessionId);
await sleep(1000); // Wait another second before second greeting
await sendNotification({
method: "notifications/message",
params: { level: "info", data: `Sending second greeting to ${name}` }
});
await server.sendLoggingMessage({
level: 'info',
data: `Sending second greeting to ${name}`
}, extra.sessionId);
return {
content: [
{
type: 'text',
text: `Good morning, ${name}!`,
text: `Good morning, ${name}!`
}
],
]
};
});
// Register a tool that demonstrates elicitation (user input collection)
// Register a tool that demonstrates form elicitation (user input collection with a schema)
// This creates a closure that captures the server instance
server.tool('collect-user-info', 'A tool that collects user information through elicitation', {
infoType: z.enum(['contact', 'preferences', 'feedback']).describe('Type of information to collect'),
}, async ({ infoType }) => {
server.registerTool('collect-user-info', {
description: 'A tool that collects user information through form elicitation',
inputSchema: {
infoType: z.enum(['contact', 'preferences', 'feedback']).describe('Type of information to collect')
}
}, async ({ infoType }, extra) => {
let message;
let requestedSchema;
switch (infoType) {
@@ -84,21 +99,21 @@ const getServer = () => {
name: {
type: 'string',
title: 'Full Name',
description: 'Your full name',
description: 'Your full name'
},
email: {
type: 'string',
title: 'Email Address',
description: 'Your email address',
format: 'email',
format: 'email'
},
phone: {
type: 'string',
title: 'Phone Number',
description: 'Your phone number (optional)',
},
description: 'Your phone number (optional)'
}
},
required: ['name', 'email'],
required: ['name', 'email']
};
break;
case 'preferences':
@@ -111,23 +126,23 @@ const getServer = () => {
title: 'Theme',
description: 'Choose your preferred theme',
enum: ['light', 'dark', 'auto'],
enumNames: ['Light', 'Dark', 'Auto'],
enumNames: ['Light', 'Dark', 'Auto']
},
notifications: {
type: 'boolean',
title: 'Enable Notifications',
description: 'Would you like to receive notifications?',
default: true,
default: true
},
frequency: {
type: 'string',
title: 'Notification Frequency',
description: 'How often would you like notifications?',
enum: ['daily', 'weekly', 'monthly'],
enumNames: ['Daily', 'Weekly', 'Monthly'],
},
enumNames: ['Daily', 'Weekly', 'Monthly']
}
},
required: ['theme'],
required: ['theme']
};
break;
case 'feedback':
@@ -140,40 +155,44 @@ const getServer = () => {
title: 'Rating',
description: 'Rate your experience (1-5)',
minimum: 1,
maximum: 5,
maximum: 5
},
comments: {
type: 'string',
title: 'Comments',
description: 'Additional comments (optional)',
maxLength: 500,
maxLength: 500
},
recommend: {
type: 'boolean',
title: 'Would you recommend this?',
description: 'Would you recommend this to others?',
},
description: 'Would you recommend this to others?'
}
},
required: ['rating', 'recommend'],
required: ['rating', 'recommend']
};
break;
default:
throw new Error(`Unknown info type: ${infoType}`);
}
try {
// Use the underlying server instance to elicit input from the client
const result = await server.server.elicitInput({
message,
requestedSchema,
});
// Use sendRequest through the extra parameter to elicit input
const result = await extra.sendRequest({
method: 'elicitation/create',
params: {
mode: 'form',
message,
requestedSchema
}
}, ElicitResultSchema);
if (result.action === 'accept') {
return {
content: [
{
type: 'text',
text: `Thank you! Collected ${infoType} information: ${JSON.stringify(result.content, null, 2)}`,
},
],
text: `Thank you! Collected ${infoType} information: ${JSON.stringify(result.content, null, 2)}`
}
]
};
}
else if (result.action === 'decline') {
@@ -181,9 +200,9 @@ const getServer = () => {
content: [
{
type: 'text',
text: `No information was collected. User declined ${infoType} information request.`,
},
],
text: `No information was collected. User declined ${infoType} information request.`
}
]
};
}
else {
@@ -191,9 +210,9 @@ const getServer = () => {
content: [
{
type: 'text',
text: `Information collection was cancelled by the user.`,
},
],
text: `Information collection was cancelled by the user.`
}
]
};
}
}
@@ -202,9 +221,9 @@ const getServer = () => {
content: [
{
type: 'text',
text: `Error collecting ${infoType} information: ${error}`,
},
],
text: `Error collecting ${infoType} information: ${error}`
}
]
};
}
});
@@ -213,8 +232,8 @@ const getServer = () => {
title: 'Greeting Template', // Display name for UI
description: 'A simple greeting prompt template',
argsSchema: {
name: z.string().describe('Name to include in greeting'),
},
name: z.string().describe('Name to include in greeting')
}
}, async ({ name }) => {
return {
messages: [
@@ -222,32 +241,32 @@ const getServer = () => {
role: 'user',
content: {
type: 'text',
text: `Please greet ${name} in a friendly manner.`,
},
},
],
text: `Please greet ${name} in a friendly manner.`
}
}
]
};
});
// Register a tool specifically for testing resumability
server.tool('start-notification-stream', 'Starts sending periodic notifications for testing resumability', {
interval: z.number().describe('Interval in milliseconds between notifications').default(100),
count: z.number().describe('Number of notifications to send (0 for 100)').default(50),
}, async ({ interval, count }, { sendNotification }) => {
server.registerTool('start-notification-stream', {
description: 'Starts sending periodic notifications for testing resumability',
inputSchema: {
interval: z.number().describe('Interval in milliseconds between notifications').default(100),
count: z.number().describe('Number of notifications to send (0 for 100)').default(50)
}
}, async ({ interval, count }, extra) => {
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
let counter = 0;
while (count === 0 || counter < count) {
counter++;
try {
await sendNotification({
method: "notifications/message",
params: {
level: "info",
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
}
});
await server.sendLoggingMessage({
level: 'info',
data: `Periodic notification #${counter} at ${new Date().toISOString()}`
}, extra.sessionId);
}
catch (error) {
console.error("Error sending notification:", error);
console.error('Error sending notification:', error);
}
// Wait for the specified interval
await sleep(interval);
@@ -256,9 +275,9 @@ const getServer = () => {
content: [
{
type: 'text',
text: `Started sending periodic notifications every ${interval}ms`,
text: `Started sending periodic notifications every ${interval}ms`
}
],
]
};
});
// Create a simple resource at a fixed URI
@@ -271,9 +290,9 @@ const getServer = () => {
contents: [
{
uri: 'https://example.com/greetings/default',
text: 'Hello, world!',
},
],
text: 'Hello, world!'
}
]
};
});
// Create additional resources for ResourceLink demonstration
@@ -286,9 +305,9 @@ const getServer = () => {
contents: [
{
uri: 'file:///example/file1.txt',
text: 'This is the content of file 1',
},
],
text: 'This is the content of file 1'
}
]
};
});
server.registerResource('example-file-2', 'file:///example/file2.txt', {
@@ -300,9 +319,9 @@ const getServer = () => {
contents: [
{
uri: 'file:///example/file2.txt',
text: 'This is the content of file 2',
},
],
text: 'This is the content of file 2'
}
]
};
});
// Register a tool that returns ResourceLinks
@@ -310,8 +329,8 @@ const getServer = () => {
title: 'List Files with ResourceLinks',
description: 'Returns a list of files as ResourceLinks without embedding their content',
inputSchema: {
includeDescriptions: z.boolean().optional().describe('Whether to include descriptions in the resource links'),
},
includeDescriptions: z.boolean().optional().describe('Whether to include descriptions in the resource links')
}
}, async ({ includeDescriptions = true }) => {
const resourceLinks = [
{
@@ -340,27 +359,60 @@ const getServer = () => {
content: [
{
type: 'text',
text: 'Here are the available files as resource links:',
text: 'Here are the available files as resource links:'
},
...resourceLinks,
{
type: 'text',
text: '\nYou can read any of these resources using their URI.',
text: '\nYou can read any of these resources using their URI.'
}
],
]
};
});
// Register a long-running tool that demonstrates task execution
// Using the experimental tasks API - WARNING: may change without notice
server.experimental.tasks.registerToolTask('delay', {
title: 'Delay',
description: 'A simple tool that delays for a specified duration, useful for testing task execution',
inputSchema: {
duration: z.number().describe('Duration in milliseconds').default(5000)
}
}, {
async createTask({ duration }, { taskStore, taskRequestedTtl }) {
// Create the task
const task = await taskStore.createTask({
ttl: taskRequestedTtl
});
// Simulate out-of-band work
(async () => {
await new Promise(resolve => setTimeout(resolve, duration));
await taskStore.storeTaskResult(task.taskId, 'completed', {
content: [
{
type: 'text',
text: `Completed ${duration}ms delay`
}
]
});
})();
// Return CreateTaskResult with the created task
return {
task
};
},
async getTask(_args, { taskId, taskStore }) {
return await taskStore.getTask(taskId);
},
async getTaskResult(_args, { taskId, taskStore }) {
const result = await taskStore.getTaskResult(taskId);
return result;
}
});
return server;
};
const MCP_PORT = process.env.MCP_PORT ? parseInt(process.env.MCP_PORT, 10) : 3000;
const AUTH_PORT = process.env.MCP_AUTH_PORT ? parseInt(process.env.MCP_AUTH_PORT, 10) : 3001;
const app = express();
app.use(express.json());
// Allow CORS all domains, expose the Mcp-Session-Id header
app.use(cors({
origin: '*', // Allow all origins
exposedHeaders: ["Mcp-Session-Id"]
}));
const app = createMcpExpressApp();
// Set up OAuth if enabled
let authMiddleware = null;
if (useOAuth) {
@@ -377,14 +429,15 @@ if (useOAuth) {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams({
token: token
}).toString()
});
if (!response.ok) {
throw new Error(`Invalid or expired token: ${await response.text()}`);
const text = await response.text().catch(() => null);
throw new Error(`Invalid or expired token: ${text}`);
}
const data = await response.json();
if (strictOAuth) {
@@ -400,7 +453,7 @@ if (useOAuth) {
token,
clientId: data.client_id,
scopes: data.scope ? data.scope.split(' ') : [],
expiresAt: data.exp,
expiresAt: data.exp
};
}
};
@@ -409,12 +462,12 @@ if (useOAuth) {
oauthMetadata,
resourceServerUrl: mcpServerUrl,
scopesSupported: ['mcp:tools'],
resourceName: 'MCP Demo Server',
resourceName: 'MCP Demo Server'
}));
authMiddleware = requireBearerAuth({
verifier: tokenVerifier,
requiredScopes: [],
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl),
resourceMetadataUrl: getOAuthProtectedResourceMetadataUrl(mcpServerUrl)
});
}
// Map to store transports by session ID
@@ -443,7 +496,7 @@ const mcpPostHandler = async (req, res) => {
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
eventStore, // Enable resumability
onsessioninitialized: (sessionId) => {
onsessioninitialized: sessionId => {
// Store the transport by session ID when session is initialized
// This avoids race conditions where requests might come in before the session is stored
console.log(`Session initialized with ID: ${sessionId}`);
@@ -471,9 +524,9 @@ const mcpPostHandler = async (req, res) => {
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Bad Request: No valid session ID provided',
message: 'Bad Request: No valid session ID provided'
},
id: null,
id: null
});
return;
}
@@ -488,9 +541,9 @@ const mcpPostHandler = async (req, res) => {
jsonrpc: '2.0',
error: {
code: -32603,
message: 'Internal server error',
message: 'Internal server error'
},
id: null,
id: null
});
}
}
@@ -556,7 +609,7 @@ if (useOAuth && authMiddleware) {
else {
app.delete('/mcp', mcpDeleteHandler);
}
app.listen(MCP_PORT, (error) => {
app.listen(MCP_PORT, error => {
if (error) {
console.error('Failed to start server:', error);
process.exit(1);