Skip to content
Merged
Show file tree
Hide file tree
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
178 changes: 95 additions & 83 deletions packages/sample-servers/src/todo-manager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ if (!MCP_RESOURCE_IDENTIFIER) {
throw new Error('MCP_RESOURCE_IDENTIFIER environment variable is required');
}

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

const assertUserId = (authInfo?: AuthInfo) => {
Expand All @@ -33,108 +34,113 @@ const hasRequiredScopes = (userScopes: string[], requiredScopes: string[]): bool
return requiredScopes.every((scope) => userScopes.includes(scope));
};

// 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',
});

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');
}
mcpServer.registerTool(
'create-todo',
{
description: 'Create a new todo',
inputSchema: { content: z.string() },
},
({ content }, { authInfo }) => {
const userId = assertUserId(authInfo);

const createdTodo = todoService.createTodo({ content, ownerId: userId });
/**
* Only users with 'create:todos' scope can create todos
*/
if (!hasRequiredScopes(authInfo?.scopes ?? [], ['create:todos'])) {
throw new MCPAuthBearerAuthError('missing_required_scopes');
}

return {
content: [{ type: 'text', text: JSON.stringify(createdTodo) }],
};
}
);
const createdTodo = todoService.createTodo({ content, ownerId: userId });

server.registerTool(
'get-todos',
{
description: 'List all todos',
inputSchema: {},
},
(_params, { 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 todos = todoService.getAllTodos(todoOwnerId);

return {
content: [{ type: 'text', text: JSON.stringify(todos) }],
};
}
);
return {
content: [{ type: 'text', text: JSON.stringify(createdTodo) }],
};
}
);

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

const todo = todoService.getTodoById(id);
/**
* 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 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;
};

const mcpAuth = new MCPAuth({
protectedResources: {
Expand All @@ -158,11 +164,17 @@ app.use(
);

app.post('/', async (request, 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();

const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
await server.connect(transport);
await mcpServer.connect(transport);
await transport.handleRequest(request, response, request.body);
response.on('close', () => {
void transport.close();
void mcpServer.close();
});
});

Expand Down
55 changes: 34 additions & 21 deletions packages/sample-servers/src/whoami/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,35 @@ import { fetchServerConfig, MCPAuth, MCPAuthTokenVerificationError } from 'mcp-a

configDotenv();

// Create an MCP server
const server = new McpServer({
name: 'WhoAmI',
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: 'WhoAmI',
version: '0.0.0',
});

// Add a tool to the server that returns the current user's information
server.registerTool(
'whoami',
{
description: 'Get the current user information',
inputSchema: {},
},
(_params, { authInfo }) => {
return {
content: [
{ type: 'text', text: JSON.stringify(authInfo?.claims ?? { error: 'Not authenticated' }) },
],
};
}
);
// Add a tool to the server that returns the current user's information
mcpServer.registerTool(
'whoami',
{
description: 'Get the current user information',
inputSchema: {},
},
(_params, { authInfo }) => {
return {
content: [
{
type: 'text',
text: JSON.stringify(authInfo?.claims ?? { error: 'Not authenticated' }),
},
],
};
}
);

return mcpServer;
};

const { MCP_AUTH_ISSUER } = process.env;

Expand Down Expand Up @@ -91,11 +99,16 @@ app.use(mcpAuth.delegatedRouter());
app.use(mcpAuth.bearerAuth(verifyToken));

app.post('/', async (request, 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();
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
await server.connect(transport);
await mcpServer.connect(transport);
await transport.handleRequest(request, response, request.body);
response.on('close', () => {
transport.close();
mcpServer.close();
});
});

Expand Down
55 changes: 34 additions & 21 deletions packages/sample-servers/src/whoami/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,35 @@ import {

configDotenv();

// Create an MCP server
const server = new McpServer({
name: 'WhoAmI',
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: 'WhoAmI',
version: '0.0.0',
});

// Add a tool to the server that returns the current user's information
server.registerTool(
'whoami',
{
description: 'Get the current user information',
inputSchema: {},
},
(_params, { authInfo }) => {
return {
content: [
{ type: 'text', text: JSON.stringify(authInfo?.claims ?? { error: 'Not authenticated' }) },
],
};
}
);
// Add a tool to the server that returns the current user's information
mcpServer.registerTool(
'whoami',
{
description: 'Get the current user information',
inputSchema: {},
},
(_params, { authInfo }) => {
return {
content: [
{
type: 'text',
text: JSON.stringify(authInfo?.claims ?? { error: 'Not authenticated' }),
},
],
};
}
);

return mcpServer;
};

const { MCP_AUTH_ISSUER } = process.env;

Expand Down Expand Up @@ -94,11 +102,16 @@ app.use(mcpAuth.delegatedRouter());
app.use(mcpAuth.bearerAuth(verifyToken));

app.post('/', async (request, 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();
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
await server.connect(transport);
await mcpServer.connect(transport);
await transport.handleRequest(request, response, request.body);
response.on('close', () => {
void transport.close();
void mcpServer.close();
});
});

Expand Down