Skip to content

Commit 6cff68d

Browse files
committed
Merge branch 'main' into mcp-stateless-ecs
2 parents f13e05a + 13db4a2 commit 6cff68d

File tree

12 files changed

+290
-12
lines changed

12 files changed

+290
-12
lines changed

samples/mcp-stateless-lambda/.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
*.js
1+
22
!jest.config.js
33
*.d.ts
44
node_modules

samples/mcp-stateless-lambda/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ Default output format [None]: json
4949
```
5050

5151
- Node.js: v18.12.1
52-
- [AWS CDK](https://github.com/aws/aws-cdk/releases/tag/v2.114.0): 2.114.0
52+
- [AWS CDK](https://github.com/aws/aws-cdk/releases/tag/v2.189.1): 2.189.1
5353
- jq: jq-1.6
5454

5555
### Deploy the solution
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
2+
export const handler = async (event) => {
3+
const authToken = event.authorizationToken;
4+
if (!authToken) {
5+
return generatePolicy('Deny', event.methodArn);
6+
}
7+
8+
if (authToken === 'Bearer good_access_token') {
9+
return generatePolicy('Allow', event.methodArn);
10+
}
11+
12+
return generatePolicy('Deny', event.methodArn);
13+
14+
};
15+
16+
const generatePolicy = (effect, resource) => {
17+
return {
18+
principalId: 'user',
19+
policyDocument: {
20+
Version: '2012-10-17',
21+
Statement: [{
22+
Action: 'execute-api:Invoke',
23+
Effect: effect,
24+
Resource: resource
25+
}]
26+
}
27+
};
28+
};
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import './logging.js';
2+
import log4js from 'log4js';
3+
import express from 'express';
4+
import metadata from './metadata.js';
5+
import transport from './transport.js';
6+
7+
await metadata.init();
8+
9+
const l = log4js.getLogger();
10+
const PORT = 3000;
11+
12+
// This function is using Lambda Web Adapter to run express.js on Lambda
13+
// https://github.com/awslabs/aws-lambda-web-adapter
14+
const app = express();
15+
app.use(express.json());
16+
17+
app.get('/health', (req, res) => {
18+
res.json(metadata.all);
19+
});
20+
21+
app.use(async (req, res, next) => {
22+
l.debug(`> ${req.method} ${req.originalUrl}`);
23+
l.debug(req.body);
24+
// l.debug(req.headers);
25+
return next();
26+
});
27+
28+
await transport.bootstrap(app);
29+
30+
app.listen(PORT, () => {
31+
l.debug(metadata.all);
32+
l.debug(`listening on http://localhost:${PORT}`);
33+
});
34+
35+
36+
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import log4js from 'log4js';
2+
3+
const layout = {
4+
type: 'pattern',
5+
pattern: '%p [%f{1}:%l:%M] %m%'
6+
}
7+
8+
log4js.configure({
9+
appenders: {
10+
stdout: {
11+
type: 'stdout',
12+
enableCallStack: true,
13+
layout
14+
}
15+
},
16+
categories: {
17+
default: {
18+
appenders: ['stdout'],
19+
level: 'debug',
20+
enableCallStack: true
21+
}
22+
}
23+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
const TEMPLATE = {
2+
jsonrpc: '2.0',
3+
error: {
4+
code: 0,
5+
message: 'n/a',
6+
},
7+
id: null,
8+
};
9+
10+
function build(code, message){
11+
const result = {...TEMPLATE};
12+
result.error.code = code;
13+
result.error.message = message;
14+
return result;
15+
}
16+
17+
18+
export default {
19+
get internalServerError(){
20+
return build(-32603, 'Internal Server Error');
21+
},
22+
23+
get noValidSessionId(){
24+
return build(-32000, 'No valid session ID');
25+
},
26+
27+
get invalidOrMissingSessionId(){
28+
return build(-32000, 'Invalid or missing session ID');
29+
},
30+
31+
get methodNotAllowed(){
32+
return build(-32000, 'Method not allowed');
33+
}
34+
35+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2+
import metadata from "./metadata.js";
3+
4+
let SHORT_DELAY = true;
5+
const LONG_DELAY_MS = 100;
6+
const SHORT_DELAY_MS = 50;
7+
8+
const create = () => {
9+
const mcpServer = new McpServer({
10+
name: "demo-mcp-server",
11+
version: metadata.version
12+
}, {
13+
capabilities: {
14+
tools: {}
15+
}
16+
});
17+
18+
mcpServer.tool("ping", async () => {
19+
const startTime = Date.now();
20+
SHORT_DELAY=!SHORT_DELAY;
21+
22+
if (SHORT_DELAY){
23+
await new Promise((resolve) => setTimeout(resolve, SHORT_DELAY_MS));
24+
} else {
25+
await new Promise((resolve) => setTimeout(resolve, LONG_DELAY_MS));
26+
}
27+
const duration = Date.now() - startTime;
28+
29+
return {
30+
content: [
31+
{
32+
type: "text",
33+
text: `pong! logStream=${metadata.logStreamName} v=${metadata.version} d=${duration}`
34+
}
35+
]
36+
}
37+
});
38+
39+
return mcpServer
40+
};
41+
42+
export default { create };
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import packageInfo from './package.json' with { type: 'json'};
2+
3+
const metadata = {
4+
}
5+
6+
async function init() {
7+
metadata.version = packageInfo.version;
8+
metadata.logStreamName = process.env.AWS_LAMBDA_LOG_STREAM_NAME || 'unknown';
9+
}
10+
11+
export default {
12+
init,
13+
14+
get all() {
15+
return metadata;
16+
},
17+
18+
get version() {
19+
return metadata.version;
20+
},
21+
22+
get logStreamName() {
23+
return metadata.logStreamName;
24+
}
25+
};
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import log4js from 'log4js';
2+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3+
import mcpServer from './mcp-server.js';
4+
import mcpErrors from './mcp-errors.js';
5+
6+
const MCP_PATH = '/mcp';
7+
8+
const l = log4js.getLogger();
9+
10+
const bootstrap = async (app) => {
11+
app.post(MCP_PATH, postRequestHandler);
12+
app.get(MCP_PATH, sessionRequestHandler);
13+
app.delete(MCP_PATH, sessionRequestHandler);
14+
}
15+
16+
const postRequestHandler = async (req, res) => {
17+
try {
18+
// Create new instances of MCP Server and Transport for each incoming request
19+
const newMcpServer = mcpServer.create();
20+
const transport = new StreamableHTTPServerTransport({
21+
// This is a stateless MCP server, so we don't need to keep track of sessions
22+
sessionIdGenerator: undefined,
23+
24+
// Change to `false` if you want to enable SSE in responses.
25+
enableJsonResponse: true,
26+
});
27+
28+
res.on('close', () => {
29+
l.debug(`request processing complete`);
30+
transport.close();
31+
newMcpServer.close();
32+
});
33+
await newMcpServer.connect(transport);
34+
await transport.handleRequest(req, res, req.body);
35+
} catch (err) {
36+
l.error(`Error handling MCP request ${err}`);
37+
if (!res.headersSent) {
38+
res.status(500).json(mcpErrors.internalServerError)
39+
}
40+
}
41+
}
42+
43+
const sessionRequestHandler = async (req, res) => {
44+
res.status(405).set('Allow', 'POST').json(mcpErrors.methodNotAllowed);
45+
}
46+
47+
export default {
48+
bootstrap
49+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
3+
4+
const ENDPOINT_URL = process.env.MCP_SERVER_ENDPOINT || 'http://localhost:3000/mcp';
5+
6+
const fake_token = 'good_access_token';
7+
8+
console.log(`Connecting ENDPOINT_URL=${ENDPOINT_URL}`);
9+
10+
const transport = new StreamableHTTPClientTransport(new URL(ENDPOINT_URL), {
11+
requestInit: {
12+
headers: {
13+
'Authorization': `Bearer ${fake_token}`
14+
}
15+
}
16+
});
17+
18+
const client = new Client({
19+
name: "node-client",
20+
version: "0.0.1"
21+
})
22+
23+
await client.connect(transport);
24+
console.log('connected');
25+
26+
const tools = await client.listTools();
27+
console.log(`listTools response: `, tools);
28+
29+
for (let i = 0; i < 2; i++) {
30+
let result = await client.callTool({
31+
name: "ping"
32+
});
33+
console.log(`callTool:ping response: `, result);
34+
}
35+
36+
await client.close();

0 commit comments

Comments
 (0)