Clean up examples (#182)
* Centralize example server transport logic in shared utils Move stdio/HTTP transport handling into `server-utils.ts` so example servers only need to call `startServer(createServer)`. Switch HTTP transport to stateless mode and remove legacy SSE endpoints. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Make example servers self-contained with local server-utils Copy `server-utils.ts` into each example's `src/` directory so examples can be used standalone without the shared directory. Updates imports from `"../shared/server-utils.js"` to `"./src/server-utils.js"`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add `integration-server` example for E2E testing Add dedicated `integration-server` example (duplicated from `basic-server-react`) to preserve E2E testing capabilities while allowing `basic-server-react` to be simplified as a minimal example. Changes: - Add `examples/integration-server` exercising SDK communication APIs - Update E2E tests to use `integration-server` instead of `basic-server-react` and `basic-server-vanillajs` - Use server name labels instead of indices for more robust test selection - Remove `basic-react.png` and `basic-vanillajs.png` golden snapshots 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Simplify `basic-server-*` examples - Clarify comments - Return plain text from `get-time` tool instead of JSON - Remove simulated cleanup delay from `onteardown` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3222c87c56
commit
9e9f5f0210
36 changed files with 1748 additions and 475 deletions
|
|
@ -1,55 +1,52 @@
|
|||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "@modelcontextprotocol/ext-apps/server";
|
||||
import { startServer } from "../shared/server-utils.js";
|
||||
import { startServer } from "./src/server-utils.js";
|
||||
|
||||
const DIST_DIR = path.join(import.meta.dirname, "dist");
|
||||
const RESOURCE_URI = "ui://get-time/mcp-app.html";
|
||||
|
||||
/**
|
||||
* Creates a new MCP server instance with tools and resources registered.
|
||||
* Each HTTP session needs its own server instance because McpServer only supports one transport.
|
||||
*/
|
||||
function createServer(): McpServer {
|
||||
const server = new McpServer({
|
||||
name: "Basic MCP App Server (React-based)",
|
||||
name: "Basic MCP App Server (React)",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
// MCP Apps require two-part registration: a tool (what the LLM calls) and a
|
||||
// resource (the UI it renders). The `_meta` field on the tool links to the
|
||||
// resource URI, telling hosts which UI to display when the tool executes.
|
||||
// Two-part registration: tool + resource, tied together by the resource URI.
|
||||
const resourceUri = "ui://get-time/mcp-app.html";
|
||||
|
||||
// Register a tool with UI metadata. When the host calls this tool, it reads
|
||||
// `_meta[RESOURCE_URI_META_KEY]` to know which resource to fetch and render
|
||||
// as an interactive UI.
|
||||
registerAppTool(server,
|
||||
"get-time",
|
||||
{
|
||||
title: "Get Time",
|
||||
description: "Returns the current server time as an ISO 8601 string.",
|
||||
inputSchema: {},
|
||||
_meta: { [RESOURCE_URI_META_KEY]: RESOURCE_URI },
|
||||
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
|
||||
},
|
||||
async (): Promise<CallToolResult> => {
|
||||
const time = new Date().toISOString();
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify({ time }) }],
|
||||
};
|
||||
return { content: [{ type: "text", text: time }] };
|
||||
},
|
||||
);
|
||||
|
||||
// Register the resource, which returns the bundled HTML/JavaScript for the UI.
|
||||
registerAppResource(server,
|
||||
RESOURCE_URI,
|
||||
RESOURCE_URI,
|
||||
resourceUri,
|
||||
resourceUri,
|
||||
{ mimeType: RESOURCE_MIME_TYPE },
|
||||
async (): Promise<ReadResourceResult> => {
|
||||
const html = await fs.readFile(path.join(DIST_DIR, "mcp-app.html"), "utf-8");
|
||||
|
||||
return {
|
||||
contents: [
|
||||
// Per the MCP App specification, "text/html;profile=mcp-app" signals
|
||||
// to the Host that this resource is indeed for an MCP App UI.
|
||||
{ uri: RESOURCE_URI, mimeType: RESOURCE_MIME_TYPE, text: html },
|
||||
{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html },
|
||||
],
|
||||
};
|
||||
},
|
||||
|
|
@ -58,16 +55,4 @@ function createServer(): McpServer {
|
|||
return server;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (process.argv.includes("--stdio")) {
|
||||
await createServer().connect(new StdioServerTransport());
|
||||
} else {
|
||||
const port = parseInt(process.env.PORT ?? "3101", 10);
|
||||
await startServer(createServer, { port, name: "Basic MCP App Server (React-based)" });
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
startServer(createServer);
|
||||
|
|
|
|||
|
|
@ -20,12 +20,8 @@ const log = {
|
|||
|
||||
|
||||
function extractTime(callToolResult: CallToolResult): string {
|
||||
const text = callToolResult.content!
|
||||
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
||||
.map((c) => c.text)
|
||||
.join("");
|
||||
const { time } = JSON.parse(text) as { time: string };
|
||||
return time;
|
||||
const { text } = callToolResult.content?.find((c) => c.type === "text")!;
|
||||
return text;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -37,8 +33,6 @@ function GetTimeApp() {
|
|||
onAppCreated: (app) => {
|
||||
app.onteardown = async () => {
|
||||
log.info("App is being torn down");
|
||||
await new Promise((resolve) => setTimeout(resolve, 500)); // Simulate cleanup work
|
||||
log.info("App teardown complete");
|
||||
return {};
|
||||
};
|
||||
app.ontoolinput = async (input) => {
|
||||
|
|
@ -120,7 +114,7 @@ function GetTimeAppInner({ app, toolResult }: GetTimeAppInnerProps) {
|
|||
|
||||
<div className={styles.action}>
|
||||
<p>
|
||||
<strong>Server Time:</strong> <code>{serverTime}</code>
|
||||
<strong>Server Time:</strong> <code id="server-time">{serverTime}</code>
|
||||
</p>
|
||||
<button onClick={handleGetTime}>Get Server Time</button>
|
||||
</div>
|
||||
|
|
|
|||
110
examples/basic-server-react/src/server-utils.ts
Normal file
110
examples/basic-server-react/src/server-utils.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
/**
|
||||
* Shared utilities for running MCP servers with various transports.
|
||||
*/
|
||||
|
||||
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import cors from "cors";
|
||||
import type { Request, Response } from "express";
|
||||
|
||||
/**
|
||||
* Starts an MCP server using the appropriate transport based on command-line arguments.
|
||||
*
|
||||
* If `--stdio` is passed, uses stdio transport. Otherwise, uses Streamable HTTP transport.
|
||||
*
|
||||
* @param createServer - Factory function that creates a new McpServer instance.
|
||||
*/
|
||||
export async function startServer(
|
||||
createServer: () => McpServer,
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (process.argv.includes("--stdio")) {
|
||||
await startStdioServer(createServer);
|
||||
} else {
|
||||
await startStreamableHttpServer(createServer);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts an MCP server with stdio transport.
|
||||
*
|
||||
* @param createServer - Factory function that creates a new McpServer instance.
|
||||
*/
|
||||
export async function startStdioServer(
|
||||
createServer: () => McpServer,
|
||||
): Promise<void> {
|
||||
await createServer().connect(new StdioServerTransport());
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts an MCP server with Streamable HTTP transport in stateless mode.
|
||||
*
|
||||
* Each request creates a fresh server and transport instance, which are
|
||||
* closed when the response ends (no session tracking).
|
||||
*
|
||||
* The server listens on the port specified by the PORT environment variable,
|
||||
* defaulting to 3001 if not set.
|
||||
*
|
||||
* @param createServer - Factory function that creates a new McpServer instance per request.
|
||||
*/
|
||||
export async function startStreamableHttpServer(
|
||||
createServer: () => McpServer,
|
||||
): Promise<void> {
|
||||
const port = parseInt(process.env.PORT ?? "3001", 10);
|
||||
|
||||
// Express app - bind to all interfaces for development/testing
|
||||
const expressApp = createMcpExpressApp({ host: "0.0.0.0" });
|
||||
expressApp.use(cors());
|
||||
|
||||
expressApp.all("/mcp", async (req: Request, res: Response) => {
|
||||
// Create fresh server and transport for each request (stateless mode)
|
||||
const server = createServer();
|
||||
const transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined,
|
||||
});
|
||||
|
||||
// Clean up when response ends
|
||||
res.on("close", () => {
|
||||
transport.close().catch(() => {});
|
||||
server.close().catch(() => {});
|
||||
});
|
||||
|
||||
try {
|
||||
await server.connect(transport);
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
} catch (error) {
|
||||
console.error("MCP error:", error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32603, message: "Internal server error" },
|
||||
id: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
||||
|
||||
const httpServer = expressApp.listen(port, (err?: Error) => {
|
||||
if (err) return reject(err);
|
||||
console.log(`Server listening on http://localhost:${port}/mcp`);
|
||||
resolve();
|
||||
});
|
||||
|
||||
const shutdown = () => {
|
||||
console.log("\nShutting down...");
|
||||
httpServer.close(() => process.exit(0));
|
||||
};
|
||||
|
||||
process.on("SIGINT", shutdown);
|
||||
process.on("SIGTERM", shutdown);
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
|
@ -1,17 +1,14 @@
|
|||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "@modelcontextprotocol/ext-apps/server";
|
||||
import { startServer } from "../shared/server-utils.js";
|
||||
import { startServer } from "./src/server-utils.js";
|
||||
|
||||
const DIST_DIR = path.join(import.meta.dirname, "dist");
|
||||
const RESOURCE_URI = "ui://get-time/mcp-app.html";
|
||||
|
||||
/**
|
||||
* Creates a new MCP server instance with tools and resources registered.
|
||||
* Each HTTP session needs its own server instance because McpServer only supports one transport.
|
||||
*/
|
||||
function createServer(): McpServer {
|
||||
const server = new McpServer({
|
||||
|
|
@ -19,37 +16,37 @@ function createServer(): McpServer {
|
|||
version: "1.0.0",
|
||||
});
|
||||
|
||||
// MCP Apps require two-part registration: a tool (what the LLM calls) and a
|
||||
// resource (the UI it renders). The `_meta` field on the tool links to the
|
||||
// resource URI, telling hosts which UI to display when the tool executes.
|
||||
// Two-part registration: tool + resource, tied together by the resource URI.
|
||||
const resourceUri = "ui://get-time/mcp-app.html";
|
||||
|
||||
// Register a tool with UI metadata. When the host calls this tool, it reads
|
||||
// `_meta[RESOURCE_URI_META_KEY]` to know which resource to fetch and render
|
||||
// as an interactive UI.
|
||||
registerAppTool(server,
|
||||
"get-time",
|
||||
{
|
||||
title: "Get Time",
|
||||
description: "Returns the current server time as an ISO 8601 string.",
|
||||
inputSchema: {},
|
||||
_meta: { [RESOURCE_URI_META_KEY]: RESOURCE_URI },
|
||||
_meta: { [RESOURCE_URI_META_KEY]: resourceUri },
|
||||
},
|
||||
async (): Promise<CallToolResult> => {
|
||||
const time = new Date().toISOString();
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify({ time }) }],
|
||||
};
|
||||
return { content: [{ type: "text", text: time }] };
|
||||
},
|
||||
);
|
||||
|
||||
// Register the resource, which returns the bundled HTML/JavaScript for the UI.
|
||||
registerAppResource(server,
|
||||
RESOURCE_URI,
|
||||
RESOURCE_URI,
|
||||
resourceUri,
|
||||
resourceUri,
|
||||
{ mimeType: RESOURCE_MIME_TYPE },
|
||||
async (): Promise<ReadResourceResult> => {
|
||||
const html = await fs.readFile(path.join(DIST_DIR, "mcp-app.html"), "utf-8");
|
||||
|
||||
return {
|
||||
contents: [
|
||||
// Per the MCP App specification, "text/html;profile=mcp-app" signals
|
||||
// to the Host that this resource is indeed for an MCP App UI.
|
||||
{ uri: RESOURCE_URI, mimeType: RESOURCE_MIME_TYPE, text: html },
|
||||
{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html },
|
||||
],
|
||||
};
|
||||
},
|
||||
|
|
@ -58,16 +55,4 @@ function createServer(): McpServer {
|
|||
return server;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (process.argv.includes("--stdio")) {
|
||||
await createServer().connect(new StdioServerTransport());
|
||||
} else {
|
||||
const port = parseInt(process.env.PORT ?? "3102", 10);
|
||||
await startServer(createServer, { port, name: "Basic MCP App Server (Vanilla JS)" });
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
startServer(createServer);
|
||||
|
|
|
|||
|
|
@ -15,12 +15,8 @@ const log = {
|
|||
|
||||
|
||||
function extractTime(result: CallToolResult): string {
|
||||
const text = result.content!
|
||||
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
||||
.map((c) => c.text)
|
||||
.join("");
|
||||
const { time } = JSON.parse(text) as { time: string };
|
||||
return time;
|
||||
const { text } = result.content?.find((c) => c.type === "text")!;
|
||||
return text;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -40,8 +36,6 @@ const app = new App({ name: "Get Time App", version: "1.0.0" });
|
|||
|
||||
app.onteardown = async () => {
|
||||
log.info("App is being torn down");
|
||||
await new Promise((resolve) => setTimeout(resolve, 500)); // Simulate cleanup work
|
||||
log.info("App teardown complete");
|
||||
return {};
|
||||
};
|
||||
|
||||
|
|
|
|||
110
examples/basic-server-vanillajs/src/server-utils.ts
Normal file
110
examples/basic-server-vanillajs/src/server-utils.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
/**
|
||||
* Shared utilities for running MCP servers with various transports.
|
||||
*/
|
||||
|
||||
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import cors from "cors";
|
||||
import type { Request, Response } from "express";
|
||||
|
||||
/**
|
||||
* Starts an MCP server using the appropriate transport based on command-line arguments.
|
||||
*
|
||||
* If `--stdio` is passed, uses stdio transport. Otherwise, uses Streamable HTTP transport.
|
||||
*
|
||||
* @param createServer - Factory function that creates a new McpServer instance.
|
||||
*/
|
||||
export async function startServer(
|
||||
createServer: () => McpServer,
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (process.argv.includes("--stdio")) {
|
||||
await startStdioServer(createServer);
|
||||
} else {
|
||||
await startStreamableHttpServer(createServer);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts an MCP server with stdio transport.
|
||||
*
|
||||
* @param createServer - Factory function that creates a new McpServer instance.
|
||||
*/
|
||||
export async function startStdioServer(
|
||||
createServer: () => McpServer,
|
||||
): Promise<void> {
|
||||
await createServer().connect(new StdioServerTransport());
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts an MCP server with Streamable HTTP transport in stateless mode.
|
||||
*
|
||||
* Each request creates a fresh server and transport instance, which are
|
||||
* closed when the response ends (no session tracking).
|
||||
*
|
||||
* The server listens on the port specified by the PORT environment variable,
|
||||
* defaulting to 3001 if not set.
|
||||
*
|
||||
* @param createServer - Factory function that creates a new McpServer instance per request.
|
||||
*/
|
||||
export async function startStreamableHttpServer(
|
||||
createServer: () => McpServer,
|
||||
): Promise<void> {
|
||||
const port = parseInt(process.env.PORT ?? "3001", 10);
|
||||
|
||||
// Express app - bind to all interfaces for development/testing
|
||||
const expressApp = createMcpExpressApp({ host: "0.0.0.0" });
|
||||
expressApp.use(cors());
|
||||
|
||||
expressApp.all("/mcp", async (req: Request, res: Response) => {
|
||||
// Create fresh server and transport for each request (stateless mode)
|
||||
const server = createServer();
|
||||
const transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined,
|
||||
});
|
||||
|
||||
// Clean up when response ends
|
||||
res.on("close", () => {
|
||||
transport.close().catch(() => {});
|
||||
server.close().catch(() => {});
|
||||
});
|
||||
|
||||
try {
|
||||
await server.connect(transport);
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
} catch (error) {
|
||||
console.error("MCP error:", error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32603, message: "Internal server error" },
|
||||
id: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
||||
|
||||
const httpServer = expressApp.listen(port, (err?: Error) => {
|
||||
if (err) return reject(err);
|
||||
console.log(`Server listening on http://localhost:${port}/mcp`);
|
||||
resolve();
|
||||
});
|
||||
|
||||
const shutdown = () => {
|
||||
console.log("\nShutting down...");
|
||||
httpServer.close(() => process.exit(0));
|
||||
};
|
||||
|
||||
process.on("SIGINT", shutdown);
|
||||
process.on("SIGTERM", shutdown);
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@
|
|||
* and industry benchmarks by company stage.
|
||||
*/
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import type {
|
||||
CallToolResult,
|
||||
ReadResourceResult,
|
||||
|
|
@ -19,7 +18,7 @@ import {
|
|||
registerAppResource,
|
||||
registerAppTool,
|
||||
} from "@modelcontextprotocol/ext-apps/server";
|
||||
import { startServer } from "../shared/server-utils.js";
|
||||
import { startServer } from "./src/server-utils.js";
|
||||
|
||||
const DIST_DIR = path.join(import.meta.dirname, "dist");
|
||||
|
||||
|
|
@ -232,7 +231,6 @@ const resourceUri = "ui://budget-allocator/mcp-app.html";
|
|||
|
||||
/**
|
||||
* Creates a new MCP server instance with tools and resources registered.
|
||||
* Each HTTP session needs its own server instance because McpServer only supports one transport.
|
||||
*/
|
||||
function createServer(): McpServer {
|
||||
const server = new McpServer({
|
||||
|
|
@ -307,20 +305,4 @@ function createServer(): McpServer {
|
|||
return server;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Server Startup
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function main() {
|
||||
if (process.argv.includes("--stdio")) {
|
||||
await createServer().connect(new StdioServerTransport());
|
||||
} else {
|
||||
const port = parseInt(process.env.PORT ?? "3103", 10);
|
||||
await startServer(createServer, { port, name: "Budget Allocator Server" });
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
startServer(createServer);
|
||||
|
|
|
|||
110
examples/budget-allocator-server/src/server-utils.ts
Normal file
110
examples/budget-allocator-server/src/server-utils.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
/**
|
||||
* Shared utilities for running MCP servers with various transports.
|
||||
*/
|
||||
|
||||
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import cors from "cors";
|
||||
import type { Request, Response } from "express";
|
||||
|
||||
/**
|
||||
* Starts an MCP server using the appropriate transport based on command-line arguments.
|
||||
*
|
||||
* If `--stdio` is passed, uses stdio transport. Otherwise, uses Streamable HTTP transport.
|
||||
*
|
||||
* @param createServer - Factory function that creates a new McpServer instance.
|
||||
*/
|
||||
export async function startServer(
|
||||
createServer: () => McpServer,
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (process.argv.includes("--stdio")) {
|
||||
await startStdioServer(createServer);
|
||||
} else {
|
||||
await startStreamableHttpServer(createServer);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts an MCP server with stdio transport.
|
||||
*
|
||||
* @param createServer - Factory function that creates a new McpServer instance.
|
||||
*/
|
||||
export async function startStdioServer(
|
||||
createServer: () => McpServer,
|
||||
): Promise<void> {
|
||||
await createServer().connect(new StdioServerTransport());
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts an MCP server with Streamable HTTP transport in stateless mode.
|
||||
*
|
||||
* Each request creates a fresh server and transport instance, which are
|
||||
* closed when the response ends (no session tracking).
|
||||
*
|
||||
* The server listens on the port specified by the PORT environment variable,
|
||||
* defaulting to 3001 if not set.
|
||||
*
|
||||
* @param createServer - Factory function that creates a new McpServer instance per request.
|
||||
*/
|
||||
export async function startStreamableHttpServer(
|
||||
createServer: () => McpServer,
|
||||
): Promise<void> {
|
||||
const port = parseInt(process.env.PORT ?? "3001", 10);
|
||||
|
||||
// Express app - bind to all interfaces for development/testing
|
||||
const expressApp = createMcpExpressApp({ host: "0.0.0.0" });
|
||||
expressApp.use(cors());
|
||||
|
||||
expressApp.all("/mcp", async (req: Request, res: Response) => {
|
||||
// Create fresh server and transport for each request (stateless mode)
|
||||
const server = createServer();
|
||||
const transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined,
|
||||
});
|
||||
|
||||
// Clean up when response ends
|
||||
res.on("close", () => {
|
||||
transport.close().catch(() => {});
|
||||
server.close().catch(() => {});
|
||||
});
|
||||
|
||||
try {
|
||||
await server.connect(transport);
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
} catch (error) {
|
||||
console.error("MCP error:", error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32603, message: "Internal server error" },
|
||||
id: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
||||
|
||||
const httpServer = expressApp.listen(port, (err?: Error) => {
|
||||
if (err) return reject(err);
|
||||
console.log(`Server listening on http://localhost:${port}/mcp`);
|
||||
resolve();
|
||||
});
|
||||
|
||||
const shutdown = () => {
|
||||
console.log("\nShutting down...");
|
||||
httpServer.close(() => process.exit(0));
|
||||
};
|
||||
|
||||
process.on("SIGINT", shutdown);
|
||||
process.on("SIGTERM", shutdown);
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import type { ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
|
@ -10,7 +9,7 @@ import {
|
|||
registerAppResource,
|
||||
registerAppTool,
|
||||
} from "@modelcontextprotocol/ext-apps/server";
|
||||
import { startServer } from "../shared/server-utils.js";
|
||||
import { startServer } from "./src/server-utils.js";
|
||||
|
||||
const DIST_DIR = path.join(import.meta.dirname, "dist");
|
||||
|
||||
|
|
@ -211,16 +210,4 @@ function createServer(): McpServer {
|
|||
return server;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (process.argv.includes("--stdio")) {
|
||||
await createServer().connect(new StdioServerTransport());
|
||||
} else {
|
||||
const port = parseInt(process.env.PORT ?? "3104", 10);
|
||||
await startServer(createServer, { port, name: "Cohort Heatmap Server" });
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
startServer(createServer);
|
||||
|
|
|
|||
110
examples/cohort-heatmap-server/src/server-utils.ts
Normal file
110
examples/cohort-heatmap-server/src/server-utils.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
/**
|
||||
* Shared utilities for running MCP servers with various transports.
|
||||
*/
|
||||
|
||||
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import cors from "cors";
|
||||
import type { Request, Response } from "express";
|
||||
|
||||
/**
|
||||
* Starts an MCP server using the appropriate transport based on command-line arguments.
|
||||
*
|
||||
* If `--stdio` is passed, uses stdio transport. Otherwise, uses Streamable HTTP transport.
|
||||
*
|
||||
* @param createServer - Factory function that creates a new McpServer instance.
|
||||
*/
|
||||
export async function startServer(
|
||||
createServer: () => McpServer,
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (process.argv.includes("--stdio")) {
|
||||
await startStdioServer(createServer);
|
||||
} else {
|
||||
await startStreamableHttpServer(createServer);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts an MCP server with stdio transport.
|
||||
*
|
||||
* @param createServer - Factory function that creates a new McpServer instance.
|
||||
*/
|
||||
export async function startStdioServer(
|
||||
createServer: () => McpServer,
|
||||
): Promise<void> {
|
||||
await createServer().connect(new StdioServerTransport());
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts an MCP server with Streamable HTTP transport in stateless mode.
|
||||
*
|
||||
* Each request creates a fresh server and transport instance, which are
|
||||
* closed when the response ends (no session tracking).
|
||||
*
|
||||
* The server listens on the port specified by the PORT environment variable,
|
||||
* defaulting to 3001 if not set.
|
||||
*
|
||||
* @param createServer - Factory function that creates a new McpServer instance per request.
|
||||
*/
|
||||
export async function startStreamableHttpServer(
|
||||
createServer: () => McpServer,
|
||||
): Promise<void> {
|
||||
const port = parseInt(process.env.PORT ?? "3001", 10);
|
||||
|
||||
// Express app - bind to all interfaces for development/testing
|
||||
const expressApp = createMcpExpressApp({ host: "0.0.0.0" });
|
||||
expressApp.use(cors());
|
||||
|
||||
expressApp.all("/mcp", async (req: Request, res: Response) => {
|
||||
// Create fresh server and transport for each request (stateless mode)
|
||||
const server = createServer();
|
||||
const transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined,
|
||||
});
|
||||
|
||||
// Clean up when response ends
|
||||
res.on("close", () => {
|
||||
transport.close().catch(() => {});
|
||||
server.close().catch(() => {});
|
||||
});
|
||||
|
||||
try {
|
||||
await server.connect(transport);
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
} catch (error) {
|
||||
console.error("MCP error:", error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32603, message: "Internal server error" },
|
||||
id: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
||||
|
||||
const httpServer = expressApp.listen(port, (err?: Error) => {
|
||||
if (err) return reject(err);
|
||||
console.log(`Server listening on http://localhost:${port}/mcp`);
|
||||
resolve();
|
||||
});
|
||||
|
||||
const shutdown = () => {
|
||||
console.log("\nShutting down...");
|
||||
httpServer.close(() => process.exit(0));
|
||||
};
|
||||
|
||||
process.on("SIGINT", shutdown);
|
||||
process.on("SIGTERM", shutdown);
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
|
@ -63,4 +63,4 @@ The tool is linked to a UI resource via `_meta.ui.resourceUri`.
|
|||
- Generates realistic customer data with Gaussian clustering around segment centers
|
||||
- Each segment has characteristic ranges for revenue, employees, engagement, etc.
|
||||
- Company names generated from word-list combinations (e.g., "Apex Data Corp")
|
||||
- Data cached in memory for session consistency
|
||||
- Data cached in memory for consistency across requests
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import type {
|
||||
CallToolResult,
|
||||
ReadResourceResult,
|
||||
|
|
@ -13,7 +12,7 @@ import {
|
|||
registerAppResource,
|
||||
registerAppTool,
|
||||
} from "@modelcontextprotocol/ext-apps/server";
|
||||
import { startServer } from "../shared/server-utils.js";
|
||||
import { startServer } from "./src/server-utils.js";
|
||||
import {
|
||||
generateCustomers,
|
||||
generateSegmentSummaries,
|
||||
|
|
@ -30,7 +29,7 @@ const GetCustomerDataInputSchema = z.object({
|
|||
.describe("Filter by segment (default: All)"),
|
||||
});
|
||||
|
||||
// Cache generated data for session consistency
|
||||
// Cache generated data for consistency across requests
|
||||
let cachedCustomers: Customer[] | null = null;
|
||||
let cachedSegments: SegmentSummary[] | null = null;
|
||||
|
||||
|
|
@ -58,7 +57,6 @@ function getCustomerData(segmentFilter?: string): {
|
|||
|
||||
/**
|
||||
* Creates a new MCP server instance with tools and resources registered.
|
||||
* Each HTTP session needs its own server instance because McpServer only supports one transport.
|
||||
*/
|
||||
function createServer(): McpServer {
|
||||
const server = new McpServer({
|
||||
|
|
@ -119,19 +117,4 @@ function createServer(): McpServer {
|
|||
return server;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (process.argv.includes("--stdio")) {
|
||||
await createServer().connect(new StdioServerTransport());
|
||||
} else {
|
||||
const port = parseInt(process.env.PORT ?? "3105", 10);
|
||||
await startServer(createServer, {
|
||||
port,
|
||||
name: "Customer Segmentation Server",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
startServer(createServer);
|
||||
|
|
|
|||
110
examples/customer-segmentation-server/src/server-utils.ts
Normal file
110
examples/customer-segmentation-server/src/server-utils.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
/**
|
||||
* Shared utilities for running MCP servers with various transports.
|
||||
*/
|
||||
|
||||
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
||||
import cors from "cors";
|
||||
import type { Request, Response } from "express";
|
||||
|
||||
/**
|
||||
* Starts an MCP server using the appropriate transport based on command-line arguments.
|
||||
*
|
||||
* If `--stdio` is passed, uses stdio transport. Otherwise, uses Streamable HTTP transport.
|
||||
*
|
||||
* @param createServer - Factory function that creates a new McpServer instance.
|
||||
*/
|
||||
export async function startServer(
|
||||
createServer: () => McpServer,
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (process.argv.includes("--stdio")) {
|
||||
await startStdioServer(createServer);
|
||||
} else {
|
||||
await startStreamableHttpServer(createServer);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts an MCP server with stdio transport.
|
||||
*
|
||||
* @param createServer - Factory function that creates a new McpServer instance.
|
||||
*/
|
||||
export async function startStdioServer(
|
||||
createServer: () => McpServer,
|
||||
): Promise<void> {
|
||||
await createServer().connect(new StdioServerTransport());
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts an MCP server with Streamable HTTP transport in stateless mode.
|
||||
*
|
||||
* Each request creates a fresh server and transport instance, which are
|
||||
* closed when the response ends (no session tracking).
|
||||
*
|
||||
* The server listens on the port specified by the PORT environment variable,
|
||||
* defaulting to 3001 if not set.
|
||||
*
|
||||
* @param createServer - Factory function that creates a new McpServer instance per request.
|
||||
*/
|
||||
export async function startStreamableHttpServer(
|
||||
createServer: () => McpServer,
|
||||
): Promise<void> {
|
||||
const port = parseInt(process.env.PORT ?? "3001", 10);
|
||||
|
||||
// Express app - bind to all interfaces for development/testing
|
||||
const expressApp = createMcpExpressApp({ host: "0.0.0.0" });
|
||||
expressApp.use(cors());
|
||||
|
||||
expressApp.all("/mcp", async (req: Request, res: Response) => {
|
||||
// Create fresh server and transport for each request (stateless mode)
|
||||
const server = createServer();
|
||||
const transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined,
|
||||
});
|
||||
|
||||
// Clean up when response ends
|
||||
res.on("close", () => {
|
||||
transport.close().catch(() => {});
|
||||
server.close().catch(() => {});
|
||||
});
|
||||
|
||||
try {
|
||||
await server.connect(transport);
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
} catch (error) {
|
||||
console.error("MCP error:", error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32603, message: "Internal server error" },
|
||||
id: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
||||
|
||||
const httpServer = expressApp.listen(port, (err?: Error) => {
|
||||
if (err) return reject(err);
|
||||
console.log(`Server listening on http://localhost:${port}/mcp`);
|
||||
resolve();
|
||||
});
|
||||
|
||||
const shutdown = () => {
|
||||
console.log("\nShutting down...");
|
||||
httpServer.close(() => process.exit(0));
|
||||
};
|
||||
|
||||
process.on("SIGINT", shutdown);
|
||||
process.on("SIGTERM", shutdown);
|
||||
|
||||
return promise;
|
||||
}
|
||||
29
examples/integration-server/README.md
Normal file
29
examples/integration-server/README.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# Example: Integration Test Server
|
||||
|
||||
An MCP App example used for E2E integration testing.
|
||||
|
||||
## Overview
|
||||
|
||||
This example demonstrates all App SDK communication APIs and is used by the E2E test suite to verify host-app interactions:
|
||||
|
||||
- Tool registration with a linked UI resource
|
||||
- React UI using the [`useApp()`](https://modelcontextprotocol.github.io/ext-apps/api/functions/_modelcontextprotocol_ext-apps_react.useApp.html) hook
|
||||
- App communication APIs: [`callServerTool`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#callservertool), [`sendMessage`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendmessage), [`sendLog`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendlog), [`openLink`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#openlink)
|
||||
|
||||
## Key Files
|
||||
|
||||
- [`server.ts`](server.ts) - MCP server with tool and resource registration
|
||||
- [`mcp-app.html`](mcp-app.html) / [`src/mcp-app.tsx`](src/mcp-app.tsx) - React UI using `useApp()` hook
|
||||
|
||||
## Getting Started
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. The server registers a `get-time` tool with metadata linking it to a UI HTML resource (`ui://get-time/mcp-app.html`).
|
||||
2. When the tool is invoked, the Host renders the UI from the resource.
|
||||
3. The UI uses the MCP App SDK API to communicate with the host and call server tools.
|
||||
14
examples/integration-server/mcp-app.html
Normal file
14
examples/integration-server/mcp-app.html
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<title>Get Time App</title>
|
||||
<link rel="stylesheet" href="/src/global.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/mcp-app.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
37
examples/integration-server/package.json
Normal file
37
examples/integration-server/package.json
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"name": "integration-server",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build",
|
||||
"watch": "cross-env INPUT=mcp-app.html vite build --watch",
|
||||
"serve:http": "bun server.ts",
|
||||
"serve:stdio": "bun server.ts --stdio",
|
||||
"start": "npm run start:http",
|
||||
"start:http": "cross-env NODE_ENV=development npm run build && npm run serve:http",
|
||||
"start:stdio": "cross-env NODE_ENV=development npm run build && npm run serve:stdio",
|
||||
"dev": "cross-env NODE_ENV=development concurrently 'npm run watch' 'npm run serve:http'"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/ext-apps": "../..",
|
||||
"@modelcontextprotocol/sdk": "^1.24.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"zod": "^4.1.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"concurrently": "^9.2.1",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.1.0",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^6.0.0",
|
||||
"vite-plugin-singlefile": "^2.3.0"
|
||||
}
|
||||
}
|
||||
67
examples/integration-server/server.ts
Normal file
67
examples/integration-server/server.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import {
|
||||
registerAppResource,
|
||||
registerAppTool,
|
||||
RESOURCE_MIME_TYPE,
|
||||
RESOURCE_URI_META_KEY,
|
||||
} from "@modelcontextprotocol/ext-apps/server";
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import type {
|
||||
CallToolResult,
|
||||
ReadResourceResult,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { startServer } from "./src/server-utils.js";
|
||||
|
||||
const DIST_DIR = path.join(import.meta.dirname, "dist");
|
||||
const RESOURCE_URI = "ui://get-time/mcp-app.html";
|
||||
|
||||
/**
|
||||
* Creates a new MCP server instance with tools and resources registered.
|
||||
*/
|
||||
function createServer(): McpServer {
|
||||
const server = new McpServer({
|
||||
name: "Integration Test Server",
|
||||
version: "1.0.0",
|
||||
});
|
||||
|
||||
registerAppTool(
|
||||
server,
|
||||
"get-time",
|
||||
{
|
||||
title: "Get Time",
|
||||
description: "Returns the current server time as an ISO 8601 string.",
|
||||
inputSchema: {},
|
||||
_meta: { [RESOURCE_URI_META_KEY]: RESOURCE_URI },
|
||||
},
|
||||
async (): Promise<CallToolResult> => {
|
||||
const time = new Date().toISOString();
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify({ time }) }],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
registerAppResource(
|
||||
server,
|
||||
RESOURCE_URI,
|
||||
RESOURCE_URI,
|
||||
{ mimeType: RESOURCE_MIME_TYPE },
|
||||
async (): Promise<ReadResourceResult> => {
|
||||
const html = await fs.readFile(
|
||||
path.join(DIST_DIR, "mcp-app.html"),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
return {
|
||||
contents: [
|
||||
{ uri: RESOURCE_URI, mimeType: RESOURCE_MIME_TYPE, text: html },
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
startServer(createServer);
|
||||
12
examples/integration-server/src/global.css
Normal file
12
examples/integration-server/src/global.css
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 1em;
|
||||
}
|
||||
65
examples/integration-server/src/mcp-app.module.css
Normal file
65
examples/integration-server/src/mcp-app.module.css
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
.main {
|
||||
--color-primary: #2563eb;
|
||||
--color-primary-hover: #1d4ed8;
|
||||
--color-notice-bg: #eff6ff;
|
||||
|
||||
width: 100%;
|
||||
max-width: 425px;
|
||||
box-sizing: border-box;
|
||||
|
||||
> * {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
> * + * {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.action {
|
||||
> * {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
> * + * {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* Consistent font for form inputs (inherits from global.css) */
|
||||
textarea,
|
||||
input {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
background-color: var(--color-primary);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
background-color: var(--color-primary-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notice {
|
||||
padding: 0.5rem 0.75rem;
|
||||
color: var(--color-primary);
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
background-color: var(--color-notice-bg);
|
||||
|
||||
&::before {
|
||||
content: "ℹ️ ";
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
167
examples/integration-server/src/mcp-app.tsx
Normal file
167
examples/integration-server/src/mcp-app.tsx
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
/**
|
||||
* @file App that demonstrates a few features using MCP Apps SDK + React.
|
||||
*/
|
||||
import type { App } from "@modelcontextprotocol/ext-apps";
|
||||
import { useApp } from "@modelcontextprotocol/ext-apps/react";
|
||||
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
||||
import { StrictMode, useCallback, useEffect, useState } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import styles from "./mcp-app.module.css";
|
||||
|
||||
const IMPLEMENTATION = { name: "Get Time App", version: "1.0.0" };
|
||||
|
||||
const log = {
|
||||
info: console.log.bind(console, "[APP]"),
|
||||
warn: console.warn.bind(console, "[APP]"),
|
||||
error: console.error.bind(console, "[APP]"),
|
||||
};
|
||||
|
||||
function extractTime(callToolResult: CallToolResult): string {
|
||||
const text = callToolResult
|
||||
.content!.filter(
|
||||
(c): c is { type: "text"; text: string } => c.type === "text",
|
||||
)
|
||||
.map((c) => c.text)
|
||||
.join("");
|
||||
const { time } = JSON.parse(text) as { time: string };
|
||||
return time;
|
||||
}
|
||||
|
||||
function GetTimeApp() {
|
||||
const [toolResult, setToolResult] = useState<CallToolResult | null>(null);
|
||||
const { app, error } = useApp({
|
||||
appInfo: IMPLEMENTATION,
|
||||
capabilities: {},
|
||||
onAppCreated: (app) => {
|
||||
app.onteardown = async () => {
|
||||
log.info("App is being torn down");
|
||||
await new Promise((resolve) => setTimeout(resolve, 500)); // Simulate cleanup work
|
||||
log.info("App teardown complete");
|
||||
return {};
|
||||
};
|
||||
app.ontoolinput = async (input) => {
|
||||
log.info("Received tool call input:", input);
|
||||
};
|
||||
|
||||
app.ontoolresult = async (result) => {
|
||||
log.info("Received tool call result:", result);
|
||||
setToolResult(result);
|
||||
};
|
||||
|
||||
app.onerror = log.error;
|
||||
},
|
||||
});
|
||||
|
||||
if (error)
|
||||
return (
|
||||
<div>
|
||||
<strong>ERROR:</strong> {error.message}
|
||||
</div>
|
||||
);
|
||||
if (!app) return <div>Connecting...</div>;
|
||||
|
||||
return <GetTimeAppInner app={app} toolResult={toolResult} />;
|
||||
}
|
||||
|
||||
interface GetTimeAppInnerProps {
|
||||
app: App;
|
||||
toolResult: CallToolResult | null;
|
||||
}
|
||||
function GetTimeAppInner({ app, toolResult }: GetTimeAppInnerProps) {
|
||||
const [serverTime, setServerTime] = useState("Loading...");
|
||||
const [messageText, setMessageText] = useState("This is message text.");
|
||||
const [logText, setLogText] = useState("This is log text.");
|
||||
const [linkUrl, setLinkUrl] = useState("https://modelcontextprotocol.io/");
|
||||
|
||||
useEffect(() => {
|
||||
if (toolResult) {
|
||||
setServerTime(extractTime(toolResult));
|
||||
}
|
||||
}, [toolResult]);
|
||||
|
||||
const handleGetTime = useCallback(async () => {
|
||||
try {
|
||||
log.info("Calling get-time tool...");
|
||||
const result = await app.callServerTool({
|
||||
name: "get-time",
|
||||
arguments: {},
|
||||
});
|
||||
log.info("get-time result:", result);
|
||||
setServerTime(extractTime(result));
|
||||
} catch (e) {
|
||||
log.error(e);
|
||||
setServerTime("[ERROR]");
|
||||
}
|
||||
}, [app]);
|
||||
|
||||
const handleSendMessage = useCallback(async () => {
|
||||
const signal = AbortSignal.timeout(5000);
|
||||
try {
|
||||
log.info("Sending message text to Host:", messageText);
|
||||
const { isError } = await app.sendMessage(
|
||||
{ role: "user", content: [{ type: "text", text: messageText }] },
|
||||
{ signal },
|
||||
);
|
||||
log.info("Message", isError ? "rejected" : "accepted");
|
||||
} catch (e) {
|
||||
log.error("Message send error:", signal.aborted ? "timed out" : e);
|
||||
}
|
||||
}, [app, messageText]);
|
||||
|
||||
const handleSendLog = useCallback(async () => {
|
||||
log.info("Sending log text to Host:", logText);
|
||||
await app.sendLog({ level: "info", data: logText });
|
||||
}, [app, logText]);
|
||||
|
||||
const handleOpenLink = useCallback(async () => {
|
||||
log.info("Sending open link request to Host:", linkUrl);
|
||||
const { isError } = await app.openLink({ url: linkUrl });
|
||||
log.info("Open link request", isError ? "rejected" : "accepted");
|
||||
}, [app, linkUrl]);
|
||||
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<p className={styles.notice}>Watch activity in the DevTools console!</p>
|
||||
|
||||
<div className={styles.action}>
|
||||
<p>
|
||||
<strong>Server Time:</strong>{" "}
|
||||
<code id="server-time">{serverTime}</code>
|
||||
</p>
|
||||
<button onClick={handleGetTime}>Get Server Time</button>
|
||||
</div>
|
||||
|
||||
<div className={styles.action}>
|
||||
<textarea
|
||||
value={messageText}
|
||||
onChange={(e) => setMessageText(e.target.value)}
|
||||
/>
|
||||
<button onClick={handleSendMessage}>Send Message</button>
|
||||
</div>
|
||||
|
||||
<div className={styles.action}>
|
||||
<input
|
||||
type="text"
|
||||
value={logText}
|
||||
onChange={(e) => setLogText(e.target.value)}
|
||||
/>
|
||||
<button onClick={handleSendLog}>Send Log</button>
|
||||
</div>
|
||||
|
||||
<div className={styles.action}>
|
||||
<input
|
||||
type="url"
|
||||
value={linkUrl}
|
||||
onChange={(e) => setLinkUrl(e.target.value)}
|
||||
/>
|
||||
<button onClick={handleOpenLink}>Open Link</button>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<GetTimeApp />
|
||||
</StrictMode>,
|
||||
);
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue