| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600 |
- "use strict";
- /**
- * Simple interactive task server demonstrating elicitation and sampling.
- *
- * This server demonstrates the task message queue pattern from the MCP Tasks spec:
- * - confirm_delete: Uses elicitation to ask the user for confirmation
- * - write_haiku: Uses sampling to request an LLM to generate content
- *
- * Both tools use the "call-now, fetch-later" pattern where the initial call
- * creates a task, and the result is fetched via tasks/result endpoint.
- */
- Object.defineProperty(exports, "__esModule", { value: true });
- const node_crypto_1 = require("node:crypto");
- const index_js_1 = require("../../server/index.js");
- const express_js_1 = require("../../server/express.js");
- const streamableHttp_js_1 = require("../../server/streamableHttp.js");
- const types_js_1 = require("../../types.js");
- const interfaces_js_1 = require("../../experimental/tasks/interfaces.js");
- const in_memory_js_1 = require("../../experimental/tasks/stores/in-memory.js");
- // ============================================================================
- // Resolver - Promise-like for passing results between async operations
- // ============================================================================
- class Resolver {
- constructor() {
- this._done = false;
- this._promise = new Promise((resolve, reject) => {
- this._resolve = resolve;
- this._reject = reject;
- });
- }
- setResult(value) {
- if (this._done)
- return;
- this._done = true;
- this._resolve(value);
- }
- setException(error) {
- if (this._done)
- return;
- this._done = true;
- this._reject(error);
- }
- wait() {
- return this._promise;
- }
- done() {
- return this._done;
- }
- }
- class TaskMessageQueueWithResolvers {
- constructor() {
- this.queues = new Map();
- this.waitResolvers = new Map();
- }
- getQueue(taskId) {
- let queue = this.queues.get(taskId);
- if (!queue) {
- queue = [];
- this.queues.set(taskId, queue);
- }
- return queue;
- }
- async enqueue(taskId, message, _sessionId, maxSize) {
- const queue = this.getQueue(taskId);
- if (maxSize !== undefined && queue.length >= maxSize) {
- throw new Error(`Task message queue overflow: queue size (${queue.length}) exceeds maximum (${maxSize})`);
- }
- queue.push(message);
- // Notify any waiters
- this.notifyWaiters(taskId);
- }
- async enqueueWithResolver(taskId, message, resolver, originalRequestId) {
- const queue = this.getQueue(taskId);
- const queuedMessage = {
- type: 'request',
- message,
- timestamp: Date.now(),
- resolver,
- originalRequestId
- };
- queue.push(queuedMessage);
- this.notifyWaiters(taskId);
- }
- async dequeue(taskId, _sessionId) {
- const queue = this.getQueue(taskId);
- return queue.shift();
- }
- async dequeueAll(taskId, _sessionId) {
- const queue = this.queues.get(taskId) ?? [];
- this.queues.delete(taskId);
- return queue;
- }
- async waitForMessage(taskId) {
- // Check if there are already messages
- const queue = this.getQueue(taskId);
- if (queue.length > 0)
- return;
- // Wait for a message to be added
- return new Promise(resolve => {
- let waiters = this.waitResolvers.get(taskId);
- if (!waiters) {
- waiters = [];
- this.waitResolvers.set(taskId, waiters);
- }
- waiters.push(resolve);
- });
- }
- notifyWaiters(taskId) {
- const waiters = this.waitResolvers.get(taskId);
- if (waiters) {
- this.waitResolvers.delete(taskId);
- for (const resolve of waiters) {
- resolve();
- }
- }
- }
- cleanup() {
- this.queues.clear();
- this.waitResolvers.clear();
- }
- }
- // ============================================================================
- // Extended task store with wait functionality
- // ============================================================================
- class TaskStoreWithNotifications extends in_memory_js_1.InMemoryTaskStore {
- constructor() {
- super(...arguments);
- this.updateResolvers = new Map();
- }
- async updateTaskStatus(taskId, status, statusMessage, sessionId) {
- await super.updateTaskStatus(taskId, status, statusMessage, sessionId);
- this.notifyUpdate(taskId);
- }
- async storeTaskResult(taskId, status, result, sessionId) {
- await super.storeTaskResult(taskId, status, result, sessionId);
- this.notifyUpdate(taskId);
- }
- async waitForUpdate(taskId) {
- return new Promise(resolve => {
- let waiters = this.updateResolvers.get(taskId);
- if (!waiters) {
- waiters = [];
- this.updateResolvers.set(taskId, waiters);
- }
- waiters.push(resolve);
- });
- }
- notifyUpdate(taskId) {
- const waiters = this.updateResolvers.get(taskId);
- if (waiters) {
- this.updateResolvers.delete(taskId);
- for (const resolve of waiters) {
- resolve();
- }
- }
- }
- }
- // ============================================================================
- // Task Result Handler - delivers queued messages and routes responses
- // ============================================================================
- class TaskResultHandler {
- constructor(store, queue) {
- this.store = store;
- this.queue = queue;
- this.pendingRequests = new Map();
- }
- async handle(taskId, server, _sessionId) {
- while (true) {
- // Get fresh task state
- const task = await this.store.getTask(taskId);
- if (!task) {
- throw new Error(`Task not found: ${taskId}`);
- }
- // Dequeue and send all pending messages
- await this.deliverQueuedMessages(taskId, server, _sessionId);
- // If task is terminal, return result
- if ((0, interfaces_js_1.isTerminal)(task.status)) {
- const result = await this.store.getTaskResult(taskId);
- // Add related-task metadata per spec
- return {
- ...result,
- _meta: {
- ...(result._meta || {}),
- [types_js_1.RELATED_TASK_META_KEY]: { taskId }
- }
- };
- }
- // Wait for task update or new message
- await this.waitForUpdate(taskId);
- }
- }
- async deliverQueuedMessages(taskId, server, _sessionId) {
- while (true) {
- const message = await this.queue.dequeue(taskId);
- if (!message)
- break;
- console.log(`[Server] Delivering queued ${message.type} message for task ${taskId}`);
- if (message.type === 'request') {
- const reqMessage = message;
- // Send the request via the server
- // Store the resolver so we can route the response back
- if (reqMessage.resolver && reqMessage.originalRequestId) {
- this.pendingRequests.set(reqMessage.originalRequestId, reqMessage.resolver);
- }
- // Send the message - for elicitation/sampling, we use the server's methods
- // But since we're in tasks/result context, we need to send via transport
- // This is simplified - in production you'd use proper message routing
- try {
- const request = reqMessage.message;
- let response;
- if (request.method === 'elicitation/create') {
- // Send elicitation request to client
- const params = request.params;
- response = await server.elicitInput(params);
- }
- else if (request.method === 'sampling/createMessage') {
- // Send sampling request to client
- const params = request.params;
- response = await server.createMessage(params);
- }
- else {
- throw new Error(`Unknown request method: ${request.method}`);
- }
- // Route response back to resolver
- if (reqMessage.resolver) {
- reqMessage.resolver.setResult(response);
- }
- }
- catch (error) {
- if (reqMessage.resolver) {
- reqMessage.resolver.setException(error instanceof Error ? error : new Error(String(error)));
- }
- }
- }
- // For notifications, we'd send them too but this example focuses on requests
- }
- }
- async waitForUpdate(taskId) {
- // Race between store update and queue message
- await Promise.race([this.store.waitForUpdate(taskId), this.queue.waitForMessage(taskId)]);
- }
- routeResponse(requestId, response) {
- const resolver = this.pendingRequests.get(requestId);
- if (resolver && !resolver.done()) {
- this.pendingRequests.delete(requestId);
- resolver.setResult(response);
- return true;
- }
- return false;
- }
- routeError(requestId, error) {
- const resolver = this.pendingRequests.get(requestId);
- if (resolver && !resolver.done()) {
- this.pendingRequests.delete(requestId);
- resolver.setException(error);
- return true;
- }
- return false;
- }
- }
- // ============================================================================
- // Task Session - wraps server to enqueue requests during task execution
- // ============================================================================
- class TaskSession {
- constructor(server, taskId, store, queue) {
- this.server = server;
- this.taskId = taskId;
- this.store = store;
- this.queue = queue;
- this.requestCounter = 0;
- }
- nextRequestId() {
- return `task-${this.taskId}-${++this.requestCounter}`;
- }
- async elicit(message, requestedSchema) {
- // Update task status to input_required
- await this.store.updateTaskStatus(this.taskId, 'input_required');
- const requestId = this.nextRequestId();
- // Build the elicitation request with related-task metadata
- const params = {
- message,
- requestedSchema,
- mode: 'form',
- _meta: {
- [types_js_1.RELATED_TASK_META_KEY]: { taskId: this.taskId }
- }
- };
- const jsonrpcRequest = {
- jsonrpc: '2.0',
- id: requestId,
- method: 'elicitation/create',
- params
- };
- // Create resolver to wait for response
- const resolver = new Resolver();
- // Enqueue the request
- await this.queue.enqueueWithResolver(this.taskId, jsonrpcRequest, resolver, requestId);
- try {
- // Wait for response
- const response = await resolver.wait();
- // Update status back to working
- await this.store.updateTaskStatus(this.taskId, 'working');
- return response;
- }
- catch (error) {
- await this.store.updateTaskStatus(this.taskId, 'working');
- throw error;
- }
- }
- async createMessage(messages, maxTokens) {
- // Update task status to input_required
- await this.store.updateTaskStatus(this.taskId, 'input_required');
- const requestId = this.nextRequestId();
- // Build the sampling request with related-task metadata
- const params = {
- messages,
- maxTokens,
- _meta: {
- [types_js_1.RELATED_TASK_META_KEY]: { taskId: this.taskId }
- }
- };
- const jsonrpcRequest = {
- jsonrpc: '2.0',
- id: requestId,
- method: 'sampling/createMessage',
- params
- };
- // Create resolver to wait for response
- const resolver = new Resolver();
- // Enqueue the request
- await this.queue.enqueueWithResolver(this.taskId, jsonrpcRequest, resolver, requestId);
- try {
- // Wait for response
- const response = await resolver.wait();
- // Update status back to working
- await this.store.updateTaskStatus(this.taskId, 'working');
- return response;
- }
- catch (error) {
- await this.store.updateTaskStatus(this.taskId, 'working');
- throw error;
- }
- }
- }
- // ============================================================================
- // Server Setup
- // ============================================================================
- const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 8000;
- // Create shared stores
- const taskStore = new TaskStoreWithNotifications();
- const messageQueue = new TaskMessageQueueWithResolvers();
- const taskResultHandler = new TaskResultHandler(taskStore, messageQueue);
- // Track active task executions
- const activeTaskExecutions = new Map();
- // Create the server
- const createServer = () => {
- const server = new index_js_1.Server({ name: 'simple-task-interactive', version: '1.0.0' }, {
- capabilities: {
- tools: {},
- tasks: {
- requests: {
- tools: { call: {} }
- }
- }
- }
- });
- // Register tools
- server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
- return {
- tools: [
- {
- name: 'confirm_delete',
- description: 'Asks for confirmation before deleting (demonstrates elicitation)',
- inputSchema: {
- type: 'object',
- properties: {
- filename: { type: 'string' }
- }
- },
- execution: { taskSupport: 'required' }
- },
- {
- name: 'write_haiku',
- description: 'Asks LLM to write a haiku (demonstrates sampling)',
- inputSchema: {
- type: 'object',
- properties: {
- topic: { type: 'string' }
- }
- },
- execution: { taskSupport: 'required' }
- }
- ]
- };
- });
- // Handle tool calls
- server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request, extra) => {
- const { name, arguments: args } = request.params;
- const taskParams = (request.params._meta?.task || request.params.task);
- // Validate task mode - these tools require tasks
- if (!taskParams) {
- throw new Error(`Tool ${name} requires task mode`);
- }
- // Create task
- const taskOptions = {
- ttl: taskParams.ttl,
- pollInterval: taskParams.pollInterval ?? 1000
- };
- const task = await taskStore.createTask(taskOptions, extra.requestId, request, extra.sessionId);
- console.log(`\n[Server] ${name} called, task created: ${task.taskId}`);
- // Start background task execution
- const taskExecution = (async () => {
- try {
- const taskSession = new TaskSession(server, task.taskId, taskStore, messageQueue);
- if (name === 'confirm_delete') {
- const filename = args?.filename ?? 'unknown.txt';
- console.log(`[Server] confirm_delete: asking about '${filename}'`);
- console.log('[Server] Sending elicitation request to client...');
- const result = await taskSession.elicit(`Are you sure you want to delete '${filename}'?`, {
- type: 'object',
- properties: {
- confirm: { type: 'boolean' }
- },
- required: ['confirm']
- });
- console.log(`[Server] Received elicitation response: action=${result.action}, content=${JSON.stringify(result.content)}`);
- let text;
- if (result.action === 'accept' && result.content) {
- const confirmed = result.content.confirm;
- text = confirmed ? `Deleted '${filename}'` : 'Deletion cancelled';
- }
- else {
- text = 'Deletion cancelled';
- }
- console.log(`[Server] Completing task with result: ${text}`);
- await taskStore.storeTaskResult(task.taskId, 'completed', {
- content: [{ type: 'text', text }]
- });
- }
- else if (name === 'write_haiku') {
- const topic = args?.topic ?? 'nature';
- console.log(`[Server] write_haiku: topic '${topic}'`);
- console.log('[Server] Sending sampling request to client...');
- const result = await taskSession.createMessage([
- {
- role: 'user',
- content: { type: 'text', text: `Write a haiku about ${topic}` }
- }
- ], 50);
- let haiku = 'No response';
- if (result.content && 'text' in result.content) {
- haiku = result.content.text;
- }
- console.log(`[Server] Received sampling response: ${haiku.substring(0, 50)}...`);
- console.log('[Server] Completing task with haiku');
- await taskStore.storeTaskResult(task.taskId, 'completed', {
- content: [{ type: 'text', text: `Haiku:\n${haiku}` }]
- });
- }
- }
- catch (error) {
- console.error(`[Server] Task ${task.taskId} failed:`, error);
- await taskStore.storeTaskResult(task.taskId, 'failed', {
- content: [{ type: 'text', text: `Error: ${error}` }],
- isError: true
- });
- }
- finally {
- activeTaskExecutions.delete(task.taskId);
- }
- })();
- activeTaskExecutions.set(task.taskId, {
- promise: taskExecution,
- server,
- sessionId: extra.sessionId ?? ''
- });
- return { task };
- });
- // Handle tasks/get
- server.setRequestHandler(types_js_1.GetTaskRequestSchema, async (request) => {
- const { taskId } = request.params;
- const task = await taskStore.getTask(taskId);
- if (!task) {
- throw new Error(`Task ${taskId} not found`);
- }
- return task;
- });
- // Handle tasks/result
- server.setRequestHandler(types_js_1.GetTaskPayloadRequestSchema, async (request, extra) => {
- const { taskId } = request.params;
- console.log(`[Server] tasks/result called for task ${taskId}`);
- return taskResultHandler.handle(taskId, server, extra.sessionId ?? '');
- });
- return server;
- };
- // ============================================================================
- // Express App Setup
- // ============================================================================
- const app = (0, express_js_1.createMcpExpressApp)();
- // Map to store transports by session ID
- const transports = {};
- // Helper to check if request is initialize
- const isInitializeRequest = (body) => {
- return typeof body === 'object' && body !== null && 'method' in body && body.method === 'initialize';
- };
- // MCP POST endpoint
- app.post('/mcp', async (req, res) => {
- const sessionId = req.headers['mcp-session-id'];
- try {
- let transport;
- if (sessionId && transports[sessionId]) {
- transport = transports[sessionId];
- }
- else if (!sessionId && isInitializeRequest(req.body)) {
- transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
- sessionIdGenerator: () => (0, node_crypto_1.randomUUID)(),
- onsessioninitialized: sid => {
- console.log(`Session initialized: ${sid}`);
- transports[sid] = transport;
- }
- });
- transport.onclose = () => {
- const sid = transport.sessionId;
- if (sid && transports[sid]) {
- console.log(`Transport closed for session ${sid}`);
- delete transports[sid];
- }
- };
- const server = createServer();
- await server.connect(transport);
- await transport.handleRequest(req, res, req.body);
- return;
- }
- else {
- res.status(400).json({
- jsonrpc: '2.0',
- error: { code: -32000, message: 'Bad Request: No valid session ID' },
- id: null
- });
- return;
- }
- await transport.handleRequest(req, res, req.body);
- }
- catch (error) {
- console.error('Error handling MCP request:', error);
- if (!res.headersSent) {
- res.status(500).json({
- jsonrpc: '2.0',
- error: { code: -32603, message: 'Internal server error' },
- id: null
- });
- }
- }
- });
- // Handle GET requests for SSE streams
- app.get('/mcp', async (req, res) => {
- const sessionId = req.headers['mcp-session-id'];
- if (!sessionId || !transports[sessionId]) {
- res.status(400).send('Invalid or missing session ID');
- return;
- }
- const transport = transports[sessionId];
- await transport.handleRequest(req, res);
- });
- // Handle DELETE requests for session termination
- app.delete('/mcp', async (req, res) => {
- const sessionId = req.headers['mcp-session-id'];
- if (!sessionId || !transports[sessionId]) {
- res.status(400).send('Invalid or missing session ID');
- return;
- }
- console.log(`Session termination request: ${sessionId}`);
- const transport = transports[sessionId];
- await transport.handleRequest(req, res);
- });
- // Start server
- app.listen(PORT, () => {
- console.log(`Starting server on http://localhost:${PORT}/mcp`);
- console.log('\nAvailable tools:');
- console.log(' - confirm_delete: Demonstrates elicitation (asks user y/n)');
- console.log(' - write_haiku: Demonstrates sampling (requests LLM completion)');
- });
- // Handle shutdown
- process.on('SIGINT', async () => {
- console.log('\nShutting down server...');
- for (const sessionId of Object.keys(transports)) {
- try {
- await transports[sessionId].close();
- delete transports[sessionId];
- }
- catch (error) {
- console.error(`Error closing session ${sessionId}:`, error);
- }
- }
- taskStore.cleanup();
- messageQueue.cleanup();
- console.log('Server shutdown complete');
- process.exit(0);
- });
- //# sourceMappingURL=simpleTaskInteractive.js.map
|