History

move to example

Rhys Sullivan committed 7 months ago

Commit d2e84cd1 parent 3d1a56a7 files · +13 −13Browse files

Showing the changes to examples (6 of 7 files). Show all

Unified · Split

6 files
  1. examples/mcp-elicitation-demo/CHANGELOG.md +3 −0
  2. examples/mcp-elicitation-demo/package.json +21 −0
  3. examples/mcp-elicitation-demo/src/index.ts +4 −0
  4. examples/mcp-elicitation-demo/src/main.ts +20 −0
  5. examples/mcp-elicitation-demo/src/server.ts +222 −0
  6. examples/mcp-elicitation-demo/tsconfig.json +15 −0
examples/mcp-elicitation-demo/CHANGELOG.md+3−0
@@ -0,0 +1,3 @@
1# @executor/mcp-elicitation-demo
2
3## null
examples/mcp-elicitation-demo/package.json+21−0
@@ -0,0 +1,21 @@
1{
2 "name": "@executor/mcp-elicitation-demo",
3 "private": true,
4 "type": "module",
5 "exports": {
6 ".": "./src/index.ts"
7 },
8 "scripts": {
9 "typecheck": "bunx tsc --noEmit -p tsconfig.json",
10 "start": "bun src/main.ts"
11 },
12 "dependencies": {
13 "@modelcontextprotocol/sdk": "^1.26.0",
14 "zod": "catalog:"
15 },
16 "devDependencies": {
17 "@types/node": "catalog:",
18 "bun-types": "catalog:"
19 },
20 "version": "0.0.0"
21}
examples/mcp-elicitation-demo/src/index.ts+4−0
@@ -0,0 +1,4 @@
1export {
2 startMcpElicitationDemoServer,
3 type McpElicitationDemoServer,
4} from "./server";
examples/mcp-elicitation-demo/src/main.ts+20−0
@@ -0,0 +1,20 @@
1import { startMcpElicitationDemoServer } from "./server";
2
3const host = process.env.HOST ?? "127.0.0.1";
4const port = process.env.PORT ? Number(process.env.PORT) : 58506;
5
6const server = await startMcpElicitationDemoServer({ host, port });
7console.error(`mcp-elicitation-demo listening on ${server.endpoint}`);
8
9const shutdown = async () => {
10 await server.close();
11 process.exit(0);
12};
13
14process.once("SIGINT", () => {
15 void shutdown();
16});
17
18process.once("SIGTERM", () => {
19 void shutdown();
20});
examples/mcp-elicitation-demo/src/server.ts+222−0
@@ -0,0 +1,222 @@
1import { randomUUID } from "node:crypto";
2
3import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
4import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
6import { z } from "zod/v4";
7
8export type McpElicitationDemoServer = {
9 readonly endpoint: string;
10 readonly close: () => Promise<void>;
11};
12
13const registerDemoTools = (server: McpServer) => {
14 server.registerTool(
15 "gated_echo",
16 {
17 description: "Ask for approval before echoing the provided value.",
18 inputSchema: {
19 value: z.string(),
20 },
21 },
22 async ({ value }: { value: string }) => {
23 const response = await server.server.elicitInput({
24 mode: "form",
25 message: `Approve gated echo for ${value}?`,
26 requestedSchema: {
27 type: "object",
28 properties: {
29 approve: {
30 type: "boolean",
31 title: "Approve",
32 },
33 },
34 required: ["approve"],
35 },
36 });
37
38 if (
39 response.action !== "accept"
40 || !response.content
41 || response.content.approve !== true
42 ) {
43 return {
44 content: [{ type: "text", text: "denied" }],
45 };
46 }
47
48 return {
49 content: [{ type: "text", text: `approved:${value}` }],
50 };
51 },
52 );
53};
54
55export const startMcpElicitationDemoServer = async (input: {
56 readonly host?: string;
57 readonly port?: number;
58} = {}): Promise<McpElicitationDemoServer> => {
59 const host = input.host ?? "127.0.0.1";
60 const app = createMcpExpressApp({ host });
61 const transports: Record<string, StreamableHTTPServerTransport> = {};
62 const servers: Record<string, McpServer> = {};
63
64 const createServer = () => {
65 const server = new McpServer(
66 {
67 name: "executor-mcp-elicitation-demo",
68 version: "1.0.0",
69 },
70 {
71 capabilities: {
72 tools: {},
73 },
74 },
75 );
76
77 registerDemoTools(server);
78 return server;
79 };
80
81 app.post("/mcp", async (req: any, res: any) => {
82 const sessionIdHeader = req.headers["mcp-session-id"];
83 const sessionId =
84 typeof sessionIdHeader === "string"
85 ? sessionIdHeader
86 : Array.isArray(sessionIdHeader)
87 ? sessionIdHeader[0]
88 : undefined;
89
90 try {
91 let transport: StreamableHTTPServerTransport;
92
93 if (sessionId && transports[sessionId]) {
94 transport = transports[sessionId];
95 } else {
96 transport = new StreamableHTTPServerTransport({
97 sessionIdGenerator: () => randomUUID(),
98 onsessioninitialized: (newSessionId) => {
99 transports[newSessionId] = transport;
100 },
101 });
102
103 transport.onclose = () => {
104 const closedSessionId = transport.sessionId;
105 if (closedSessionId && transports[closedSessionId]) {
106 delete transports[closedSessionId];
107 }
108 if (closedSessionId && servers[closedSessionId]) {
109 void servers[closedSessionId].close().catch(() => undefined);
110 delete servers[closedSessionId];
111 }
112 };
113
114 const server = createServer();
115 await server.connect(transport);
116 const newSessionId = transport.sessionId;
117 if (newSessionId) {
118 servers[newSessionId] = server;
119 }
120 }
121
122 await transport.handleRequest(req, res, req.body);
123 } catch (error) {
124 if (!res.headersSent) {
125 res.status(500).json({
126 jsonrpc: "2.0",
127 error: {
128 code: -32603,
129 message: error instanceof Error ? error.message : "Internal server error",
130 },
131 id: null,
132 });
133 }
134 }
135 });
136
137 app.get("/mcp", async (req: any, res: any) => {
138 const sessionIdHeader = req.headers["mcp-session-id"];
139 const sessionId =
140 typeof sessionIdHeader === "string"
141 ? sessionIdHeader
142 : Array.isArray(sessionIdHeader)
143 ? sessionIdHeader[0]
144 : undefined;
145
146 if (!sessionId || !transports[sessionId]) {
147 res.status(400).send("Invalid or missing session ID");
148 return;
149 }
150
151 await transports[sessionId].handleRequest(req, res);
152 });
153
154 app.delete("/mcp", async (req: any, res: any) => {
155 const sessionIdHeader = req.headers["mcp-session-id"];
156 const sessionId =
157 typeof sessionIdHeader === "string"
158 ? sessionIdHeader
159 : Array.isArray(sessionIdHeader)
160 ? sessionIdHeader[0]
161 : undefined;
162
163 if (!sessionId || !transports[sessionId]) {
164 res.status(400).send("Invalid or missing session ID");
165 return;
166 }
167
168 const transport = transports[sessionId];
169 await transport.handleRequest(req, res, req.body);
170 await transport.close();
171 delete transports[sessionId];
172
173 if (servers[sessionId]) {
174 await servers[sessionId].close().catch(() => undefined);
175 delete servers[sessionId];
176 }
177 });
178
179 const listener = await new Promise<import("node:http").Server>((resolve, reject) => {
180 const server = app.listen(input.port ?? 0, host);
181
182 const onListening = () => {
183 server.off("error", onError);
184 resolve(server);
185 };
186
187 const onError = (error: Error) => {
188 server.off("listening", onListening);
189 reject(error);
190 };
191
192 server.once("listening", onListening);
193 server.once("error", onError);
194 });
195
196 const address = listener.address();
197 if (!address || typeof address === "string") {
198 if (listener.listening) {
199 await new Promise<void>((resolve, reject) => {
200 listener.close((error) => (error ? reject(error) : resolve()));
201 });
202 }
203 throw new Error("Failed to resolve MCP elicitation demo server address");
204 }
205
206 return {
207 endpoint: `http://${host}:${address.port}/mcp`,
208 close: async () => {
209 for (const transport of Object.values(transports)) {
210 await transport.close().catch(() => undefined);
211 }
212
213 for (const server of Object.values(servers)) {
214 await server.close().catch(() => undefined);
215 }
216
217 await new Promise<void>((resolve, reject) => {
218 listener.close((error) => (error ? reject(error) : resolve()));
219 });
220 },
221 };
222};
examples/mcp-elicitation-demo/tsconfig.json+15−0
@@ -0,0 +1,15 @@
1{
2 "compilerOptions": {
3 "target": "ES2022",
4 "module": "ESNext",
5 "moduleResolution": "Bundler",
6 "strict": true,
7 "skipLibCheck": true,
8 "noEmit": true,
9 "lib": ["ES2022"],
10 "types": ["bun-types", "node"]
11 },
12 "include": [
13 "src/**/*.ts"
14 ]
15}