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 @@
1+export {
2+ startMcpElicitationDemoServer,
3+ type McpElicitationDemoServer,
4+} from "./server";
examples/mcp-elicitation-demo/src/main.ts+20−0
@@ -0,0 +1,20 @@
1+import { startMcpElicitationDemoServer } from "./server";
2+
3+const host = process.env.HOST ?? "127.0.0.1";
4+const port = process.env.PORT ? Number(process.env.PORT) : 58506;
5+
6+const server = await startMcpElicitationDemoServer({ host, port });
7+console.error(`mcp-elicitation-demo listening on ${server.endpoint}`);
8+
9+const shutdown = async () => {
10+ await server.close();
11+ process.exit(0);
12+};
13+
14+process.once("SIGINT", () => {
15+ void shutdown();
16+});
17+
18+process.once("SIGTERM", () => {
19+ void shutdown();
20+});
examples/mcp-elicitation-demo/src/server.ts+222−0
@@ -0,0 +1,222 @@
1+import { randomUUID } from "node:crypto";
2+
3+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
4+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
6+import { z } from "zod/v4";
7+
8+export type McpElicitationDemoServer = {
9+ readonly endpoint: string;
10+ readonly close: () => Promise<void>;
11+};
12+
13+const 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+
55+export 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+}