Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
292 changes: 140 additions & 152 deletions docs/tutorials/todo-manager/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -338,50 +338,55 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import express, { type Request, type Response } from 'express';

// Create an MCP server
const server = new McpServer({
name: 'Todo Manager',
version: '0.0.0',
});
// Factory function to create an MCP server instance
// In stateless mode, each request needs its own server instance
const createMcpServer = () => {
const mcpServer = new McpServer({
name: 'Todo Manager',
version: '0.0.0',
});

mcpServer.registerTool(
'create-todo',
{
description: 'Create a new todo',
inputSchema: { content: z.string() },
},
async ({ content }) => {
return {
content: [{ type: 'text', text: JSON.stringify({ error: 'Not implemented' }) }],
};
}
);

server.registerTool(
'create-todo',
{
description: 'Create a new todo',
inputSchema: { content: z.string() },
},
async ({ content }) => {
return {
content: [{ type: 'text', text: JSON.stringify({ error: 'Not implemented' }) }],
};
}
);
mcpServer.registerTool(
'get-todos',
{
description: 'List all todos',
inputSchema: {},
},
async () => {
return {
content: [{ type: 'text', text: JSON.stringify({ error: 'Not implemented' }) }],
};
}
);

server.registerTool(
'get-todos',
{
description: 'List all todos',
inputSchema: {},
},
async () => {
return {
content: [{ type: 'text', text: JSON.stringify({ error: 'Not implemented' }) }],
};
}
);
mcpServer.registerTool(
'delete-todo',
{
description: 'Delete a todo by id',
inputSchema: { id: z.string() },
},
async ({ id }) => {
return {
content: [{ type: 'text', text: JSON.stringify({ error: 'Not implemented' }) }],
};
}
);

server.registerTool(
'delete-todo',
{
description: 'Delete a todo by id',
inputSchema: { id: z.string() },
},
async ({ id }) => {
return {
content: [{ type: 'text', text: JSON.stringify({ error: 'Not implemented' }) }],
};
}
);
return mcpServer;
};

// Below is the boilerplate code from MCP SDK documentation
const PORT = 3001;
Expand All @@ -391,18 +396,19 @@ app.post('/', async (request: Request, response: Response) => {
// In stateless mode, create a new instance of transport and server for each request
// to ensure complete isolation. A single instance would cause request ID collisions
// when multiple clients connect concurrently.
const mcpServer = createMcpServer();

try {
const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
response.on('close', async () => {
await mcpServer.connect(transport);
await transport.handleRequest(request, response, request.body);
response.on('close', () => {
console.log('Request closed');
await transport.close();
await server.close();
void transport.close();
void mcpServer.close();
});
await server.connect(transport);
await transport.handleRequest(request, response, request.body);
} catch (error) {
console.error('Error handling MCP request:', error);
if (!response.headersSent) {
Expand All @@ -418,36 +424,6 @@ app.post('/', async (request: Request, response: Response) => {
}
});

// SSE notifications not supported in stateless mode
app.get('/', async (request: Request, response: Response) => {
console.log('Received GET MCP request');
response.writeHead(405).end(
JSON.stringify({
jsonrpc: '2.0',
error: {
code: -32_000,
message: 'Method not allowed.',
},
id: null,
})
);
});

// Session termination not needed in stateless mode
app.delete('/', async (request: Request, response: Response) => {
console.log('Received DELETE MCP request');
response.writeHead(405).end(
JSON.stringify({
jsonrpc: '2.0',
error: {
code: -32_000,
message: 'Method not allowed.',
},
id: null,
})
);
});

app.listen(PORT);
```

Expand Down Expand Up @@ -715,104 +691,116 @@ const hasRequiredScopes = (userScopes: string[], requiredScopes: string[]): bool
return requiredScopes.every((scope) => userScopes.includes(scope));
};

// TodoService is a singleton since we need to share state across requests
const todoService = new TodoService();

server.registerTool(
'create-todo',
{
description: 'Create a new todo',
inputSchema: { content: z.string() },
},
({ content }, { authInfo }) => {
const userId = assertUserId(authInfo);

/**
* Only users with 'create:todos' scope can create todos
*/
if (!hasRequiredScopes(authInfo?.scopes ?? [], ['create:todos'])) {
throw new MCPAuthBearerAuthError('missing_required_scopes');
}
// Factory function to create an MCP server instance
// In stateless mode, each request needs its own server instance
const createMcpServer = () => {
const mcpServer = new McpServer({
name: 'Todo Manager',
version: '0.0.0',
});

const createdTodo = todoService.createTodo({ content, ownerId: userId });

return {
content: [{ type: 'text', text: JSON.stringify(createdTodo) }],
};
}
);
mcpServer.registerTool(
'create-todo',
{
description: 'Create a new todo',
inputSchema: { content: z.string() },
},
({ content }, { authInfo }) => {
const userId = assertUserId(authInfo);

server.registerTool(
'get-todos',
{
description: 'List all todos',
inputSchema: {},
},
(_params, { authInfo }) => {
const userId = assertUserId(authInfo);
/**
* Only users with 'create:todos' scope can create todos
*/
if (!hasRequiredScopes(authInfo?.scopes ?? [], ['create:todos'])) {
throw new MCPAuthBearerAuthError('missing_required_scopes');
}

/**
* If user has 'read:todos' scope, they can access all todos (todoOwnerId = undefined)
* If user doesn't have 'read:todos' scope, they can only access their own todos (todoOwnerId = userId)
*/
const todoOwnerId = hasRequiredScopes(authInfo?.scopes ?? [], ['read:todos'])
? undefined
: userId;
const createdTodo = todoService.createTodo({ content, ownerId: userId });

const todos = todoService.getAllTodos(todoOwnerId);
return {
content: [{ type: 'text', text: JSON.stringify(createdTodo) }],
};
}
);

return {
content: [{ type: 'text', text: JSON.stringify(todos) }],
};
}
);
mcpServer.registerTool(
'get-todos',
{
description: 'List all todos',
inputSchema: {},
},
(_params, { authInfo }) => {
const userId = assertUserId(authInfo);

server.registerTool(
'delete-todo',
{
description: 'Delete a todo by id',
inputSchema: { id: z.string() },
},
({ id }, { authInfo }) => {
const userId = assertUserId(authInfo);
/**
* If user has 'read:todos' scope, they can access all todos (todoOwnerId = undefined)
* If user doesn't have 'read:todos' scope, they can only access their own todos (todoOwnerId = userId)
*/
const todoOwnerId = hasRequiredScopes(authInfo?.scopes ?? [], ['read:todos'])
? undefined
: userId;

const todo = todoService.getTodoById(id);
const todos = todoService.getAllTodos(todoOwnerId);

if (!todo) {
return {
content: [{ type: 'text', text: JSON.stringify({ error: 'Failed to delete todo' }) }],
content: [{ type: 'text', text: JSON.stringify(todos) }],
};
}
);

mcpServer.registerTool(
'delete-todo',
{
description: 'Delete a todo by id',
inputSchema: { id: z.string() },
},
({ id }, { authInfo }) => {
const userId = assertUserId(authInfo);

const todo = todoService.getTodoById(id);

if (!todo) {
return {
content: [{ type: 'text', text: JSON.stringify({ error: 'Failed to delete todo' }) }],
};
}

/**
* Users can only delete their own todos
* Users with 'delete:todos' scope can delete any todo
*/
if (todo.ownerId !== userId && !hasRequiredScopes(authInfo?.scopes ?? [], ['delete:todos'])) {
return {
content: [
{
type: 'text',
text: JSON.stringify({ error: 'Failed to delete todo' }),
},
],
};
}

const deletedTodo = todoService.deleteTodo(id);

/**
* Users can only delete their own todos
* Users with 'delete:todos' scope can delete any todo
*/
if (todo.ownerId !== userId && !hasRequiredScopes(authInfo?.scopes ?? [], ['delete:todos'])) {
return {
content: [
{
type: 'text',
text: JSON.stringify({ error: 'Failed to delete todo' }),
text: JSON.stringify({
message: `Todo ${id} deleted`,
details: deletedTodo,
}),
},
],
};
}
);

const deletedTodo = todoService.deleteTodo(id);

return {
content: [
{
type: 'text',
text: JSON.stringify({
message: `Todo ${id} deleted`,
details: deletedTodo,
}),
},
],
};
}
);
return mcpServer;
};
```

Now, create the "Todo service" used in the above code to implement the related functionality:
Expand Down