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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,8 @@ You can test the MCP server using the MCP Inspector:
2. In your browser, open the MCP Inspector (the URL will be shown in the terminal)

3. Configure the connection:
- **Transport**: Streamable HTTP or SSE
- **URL**: `http://localhost:3000/mcp` (for Streamable HTTP) or `http://localhost:3000/sse` (for legacy SSE)
- **Transport**: Streamable HTTP
- **URL**: `http://localhost:3000/mcp`

4. Click **Connect** and explore the available tools

Expand Down
2 changes: 1 addition & 1 deletion docs/blog/01-release/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ Before we play a bit with the sample, let's have a look at the main services imp
| ------- | ---- | ---- |
| Agent Web App (`agent-webapp`) | Chat UI + streaming + session history | Azure Static Web Apps, Lit web components |
| Agent API (`agent-api`) | LangChain.js v1 agent orchestration + auth + history | Azure Functions, Node.js |
| Burger MCP Server (`burger-mcp`) | Exposes burger API as tools over MCP (Streamable HTTP + SSE) | Azure Functions, Express, MCP SDK |
| Burger MCP Server (`burger-mcp`) | Exposes burger API as tools over MCP (Streamable HTTP) | Azure Functions, Express, MCP SDK |
| Burger API (`burger-api`) | Business logic: burgers, toppings, orders lifecycle | Azure Functions, Cosmos DB |

Here's a simplified view of how they interact:
Expand Down
6,054 changes: 2,945 additions & 3,109 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/agent-webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"dependencies": {
"dompurify": "^3.0.0",
"lit": "^3.0.0",
"marked": "^16.2.1"
"marked": "^17.0.0"
},
"devDependencies": {
"@types/dompurify": "^3.0.0",
Expand Down
6 changes: 1 addition & 5 deletions packages/burger-mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ This is the Burger MCP server, exposing the Burger API as a Model Context Protoc
This server supports the following transport types:

- **Streamable HTTP**
- **SSE** (legacy protocol, for backward compatibility)
- **Stdio** (currently only supported when starting the server locally with `npm start:local`)

The remote server is deployed with [Azure Functions](https://learn.microsoft.com/azure/azure-functions/functions-overview).
Expand Down Expand Up @@ -56,9 +55,6 @@ First, you need to start the Burger API and Burger MCP server locally.
4. Put `http://localhost:3000/mcp` in the URL field and click on the **Connect** button.
5. In the **Tools** tab, select **List Tools**. Click on a tool and select **Run Tool**.

> [!NOTE]
> This application also provides an SSE endpoint if you use `/sse` instead of `/mcp` in the URL field.

## Development

### Getting started
Expand All @@ -73,7 +69,7 @@ You can run the following command to run the application server:
npm start
```

This will start the application server. The MCP server is then available at `http://localhost:3000/mcp` or `http://localhost:3000/sse` for the streamable HTTP and SSE endpoints, respectively.
This will start the application server. The MCP server is then available at `http://localhost:3000/mcp` for the streamable HTTP endpoints.

Alternatively, you can run the MCP server with stdio transport using:

Expand Down
113 changes: 63 additions & 50 deletions packages/burger-mcp/src/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,56 +26,69 @@ export function createMcpTool<T extends z.ZodTypeAny>(
},
) {
if (options.schema) {
server.tool(options.name, options.description, options.schema.shape, async (args: z.ZodRawShape) => {
try {
const result = await options.handler(args);
return {
content: [
{
type: 'text',
text: result,
},
],
};
} catch (error: any) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error('Error executing MCP tool:', errorMessage);
return {
content: [
{
type: 'text',
text: `Error: ${errorMessage}`,
},
],
isError: true,
};
}
});
server.registerTool(
options.name,
{
description: options.description,
inputSchema: options.schema,
},
async (args: z.ZodRawShape) => {
try {
const result = await options.handler(args);
return {
content: [
{
type: 'text' as const,
text: result,
},
],
};
} catch (error: any) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error('Error executing MCP tool:', errorMessage);
return {
content: [
{
type: 'text' as const,
text: `Error: ${errorMessage}`,
},
],
isError: true,
};
}
},
);
} else {
server.tool(options.name, options.description, async () => {
try {
const result = await options.handler(undefined as any);
return {
content: [
{
type: 'text',
text: result,
},
],
};
} catch (error: any) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error('Error executing MCP tool:', errorMessage);
return {
content: [
{
type: 'text',
text: `Error: ${errorMessage}`,
},
],
isError: true,
};
}
});
server.registerTool(
options.name,
{
description: options.description,
},
async () => {
try {
const result = await options.handler(undefined as any);
return {
content: [
{
type: 'text' as const,
text: result,
},
],
};
} catch (error: any) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error('Error executing MCP tool:', errorMessage);
return {
content: [
{
type: 'text' as const,
text: `Error: ${errorMessage}`,
},
],
isError: true,
};
}
},
);
}
}
144 changes: 24 additions & 120 deletions packages/burger-mcp/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import process from 'node:process';
import { randomUUID } from 'node:crypto';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
import express, { Request, Response } from 'express';
import { burgerApiUrl } from './config.js';
import { getMcpServer } from './mcp.js';
Expand All @@ -14,77 +11,42 @@ app.get('/', (_request: Request, response: Response) => {
response.send({ status: 'up', message: `Burger MCP server running (Using burger API URL: ${burgerApiUrl})` });
});

// Store transports by session ID
const transports: Record<string, StreamableHTTPServerTransport | SSEServerTransport> = {};

// ----------------------------------------------------------------------------
// New streamable HTTP transport
// ----------------------------------------------------------------------------

// Handle all MCP Streamable HTTP requests (GET, POST, DELETE) on a single endpoint
app.all('/mcp', async (request: Request, response: Response) => {
console.log(`Received ${request.method} request to /mcp`);

try {
// Check for existing session ID
const sessionId = request.headers['mcp-session-id'] as string | undefined;
let transport: StreamableHTTPServerTransport;

if (sessionId && transports[sessionId]) {
// Check if the transport is of the correct type
const existingTransport = transports[sessionId];
if (existingTransport instanceof StreamableHTTPServerTransport) {
// Reuse existing transport
transport = existingTransport;
} else {
// Transport exists but is not a StreamableHTTPServerTransport (could be SSEServerTransport)
response.status(400).json({
jsonrpc: '2.0',
error: {
code: -32_000,
message: 'Bad Request: Session exists but uses a different transport protocol',
},
id: null,
});
return;
}
} else if (!sessionId && request.method === 'POST' && isInitializeRequest(request.body)) {
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized(sessionId) {
// Store the transport by session ID when session is initialized
console.log(`StreamableHTTP session initialized with ID: ${sessionId}`);
transports[sessionId] = transport;
},
});

// Set up onclose handler to clean up transport when closed
transport.onclose = () => {
const sid = transport.sessionId;
if (sid && transports[sid]) {
console.log(`Transport closed for session ${sid}, removing from transports map`);
delete transports[sid];
}
};

// Connect the transport to the MCP server
const server = getMcpServer();
await server.connect(transport);
} else {
// Invalid request - no session ID or not initialization request
response.status(400).json({
// Reject unsupported methods (these are only needed for stateful sessions)
if (request.method === 'GET' || request.method === 'DELETE') {
response.writeHead(405).end(
JSON.stringify({
jsonrpc: '2.0',
error: {
code: -32_000,
message: 'Bad Request: No valid session ID provided',
message: 'Method not allowed.',
},
id: null,
});
return;
}
}),
);
return;
}

try {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});

// Connect the transport to the MCP server
const server = getMcpServer();
await server.connect(transport);

// Handle the request with the transport
await transport.handleRequest(request, response, request.body);

// Clean up when the response is closed
response.on('close', async () => {
await transport.close();
await server.close();
});
} catch (error) {
console.error('Error handling MCP request:', error);
if (!response.headersSent) {
Expand All @@ -100,48 +62,6 @@ app.all('/mcp', async (request: Request, response: Response) => {
}
});

// ----------------------------------------------------------------------------
// Deprecated SSE transport
// ----------------------------------------------------------------------------

app.get('/sse', async (request: Request, response: Response) => {
console.log('Received GET request to /sse (deprecated SSE transport)');
const transport = new SSEServerTransport('/messages', response);
transports[transport.sessionId] = transport;
response.on('close', () => {
delete transports[transport.sessionId];
});
const server = getMcpServer();
await server.connect(transport);
});

app.post('/messages', async (request: Request, response: Response) => {
const sessionId = request.query.sessionId as string;
let transport: SSEServerTransport;
const existingTransport = transports[sessionId];
if (existingTransport instanceof SSEServerTransport) {
// Reuse existing transport
transport = existingTransport;
} else {
// Transport exists but is not a SSEServerTransport (could be StreamableHTTPServerTransport)
response.status(400).json({
jsonrpc: '2.0',
error: {
code: -32_000,
message: 'Bad Request: Session exists but uses a different transport protocol',
},
id: null,
});
return;
}

if (transport) {
await transport.handlePostMessage(request, response, request.body);
} else {
response.status(400).send('No transport found for sessionId');
}
});

// Start the server
const PORT = process.env.FUNCTIONS_CUSTOMHANDLER_PORT || process.env.PORT || 3000;
app.listen(PORT, () => {
Expand All @@ -151,21 +71,5 @@ app.listen(PORT, () => {
// Handle server shutdown
process.on('SIGINT', async () => {
console.log('Shutting down server...');

// Close all active transports to properly clean up resources
for (const sessionId in transports) {
if (Object.hasOwn(transports, sessionId)) {
try {
console.log(`Closing transport for session ${sessionId}`);
// eslint-disable-next-line no-await-in-loop
await transports[sessionId].close();
delete transports[sessionId];
} catch (error) {
console.error(`Error closing transport for session ${sessionId}:`, error);
}
}
}

console.log('Server shutdown complete');
process.exit(0);
});