feat: add server helpers, make connect() to default to parent post transport (#165)
* feat: add server helpers and optional connect() transport Add `src/server/` with convenience functions for registering MCP App tools and resources: - `registerAppTool(server, name, config, handler)` - `registerAppResource(server, name, uri, config, callback)` The `transport` parameter in `App.connect()` is now optional, defaulting to `PostMessageTransport(window.parent)`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Update src/server/index.ts Co-authored-by: Jonathan Hefner <jonathan@hefner.pro> * fix: refine server helper types - Make RESOURCE_URI_META_KEY required in McpUiAppToolConfig (tools need a UI resource) - Make _meta optional in McpUiAppResourceConfig (not all resources need custom metadata) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: simplify RESOURCE_URI_META_KEY re-export * refactor: consolidate export statements --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>
This commit is contained in:
parent
50cf6a4679
commit
bea0ae27f5
20 changed files with 407 additions and 40 deletions
|
|
@ -3,7 +3,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
||||||
import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
|
import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
|
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 "../shared/server-utils.js";
|
||||||
|
|
||||||
const DIST_DIR = path.join(import.meta.dirname, "dist");
|
const DIST_DIR = path.join(import.meta.dirname, "dist");
|
||||||
|
|
@ -22,7 +22,7 @@ function createServer(): McpServer {
|
||||||
// MCP Apps require two-part registration: a tool (what the LLM calls) and a
|
// 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 (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.
|
// resource URI, telling hosts which UI to display when the tool executes.
|
||||||
server.registerTool(
|
registerAppTool(server,
|
||||||
"get-time",
|
"get-time",
|
||||||
{
|
{
|
||||||
title: "Get Time",
|
title: "Get Time",
|
||||||
|
|
@ -38,7 +38,7 @@ function createServer(): McpServer {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
server.registerResource(
|
registerAppResource(server,
|
||||||
RESOURCE_URI,
|
RESOURCE_URI,
|
||||||
RESOURCE_URI,
|
RESOURCE_URI,
|
||||||
{ mimeType: RESOURCE_MIME_TYPE },
|
{ mimeType: RESOURCE_MIME_TYPE },
|
||||||
|
|
|
||||||
|
|
@ -52,5 +52,5 @@ button.addEventListener("click", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Connect to host
|
// Connect to host
|
||||||
app.connect(new PostMessageTransport(window.parent));
|
app.connect();
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
||||||
import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
|
import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
|
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 "../shared/server-utils.js";
|
||||||
|
|
||||||
const DIST_DIR = path.join(import.meta.dirname, "dist");
|
const DIST_DIR = path.join(import.meta.dirname, "dist");
|
||||||
|
|
@ -22,7 +22,7 @@ function createServer(): McpServer {
|
||||||
// MCP Apps require two-part registration: a tool (what the LLM calls) and a
|
// 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 (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.
|
// resource URI, telling hosts which UI to display when the tool executes.
|
||||||
server.registerTool(
|
registerAppTool(server,
|
||||||
"get-time",
|
"get-time",
|
||||||
{
|
{
|
||||||
title: "Get Time",
|
title: "Get Time",
|
||||||
|
|
@ -38,7 +38,7 @@ function createServer(): McpServer {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
server.registerResource(
|
registerAppResource(server,
|
||||||
RESOURCE_URI,
|
RESOURCE_URI,
|
||||||
RESOURCE_URI,
|
RESOURCE_URI,
|
||||||
{ mimeType: RESOURCE_MIME_TYPE },
|
{ mimeType: RESOURCE_MIME_TYPE },
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* @file App that demonstrates a few features using MCP Apps SDK with vanilla JS.
|
* @file App that demonstrates a few features using MCP Apps SDK with vanilla JS.
|
||||||
*/
|
*/
|
||||||
import { App, PostMessageTransport } from "@modelcontextprotocol/ext-apps";
|
import { App } from "@modelcontextprotocol/ext-apps";
|
||||||
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
||||||
import "./global.css";
|
import "./global.css";
|
||||||
import "./mcp-app.css";
|
import "./mcp-app.css";
|
||||||
|
|
@ -98,4 +98,4 @@ openLinkBtn.addEventListener("click", async () => {
|
||||||
|
|
||||||
|
|
||||||
// Connect to host
|
// Connect to host
|
||||||
app.connect(new PostMessageTransport(window.parent));
|
app.connect();
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,10 @@ import type {
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
|
import {
|
||||||
|
RESOURCE_MIME_TYPE,
|
||||||
|
RESOURCE_URI_META_KEY,
|
||||||
|
} from "@modelcontextprotocol/ext-apps/server";
|
||||||
import { startServer } from "../shared/server-utils.js";
|
import { startServer } from "../shared/server-utils.js";
|
||||||
|
|
||||||
const DIST_DIR = path.join(import.meta.dirname, "dist");
|
const DIST_DIR = path.join(import.meta.dirname, "dist");
|
||||||
|
|
@ -235,7 +238,8 @@ function createServer(): McpServer {
|
||||||
version: "1.0.0",
|
version: "1.0.0",
|
||||||
});
|
});
|
||||||
|
|
||||||
server.registerTool(
|
registerAppTool(
|
||||||
|
server,
|
||||||
"get-budget-data",
|
"get-budget-data",
|
||||||
{
|
{
|
||||||
title: "Get Budget Data",
|
title: "Get Budget Data",
|
||||||
|
|
@ -277,7 +281,8 @@ function createServer(): McpServer {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
server.registerResource(
|
registerAppResource(
|
||||||
|
server,
|
||||||
resourceUri,
|
resourceUri,
|
||||||
resourceUri,
|
resourceUri,
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Budget Allocator App - Interactive budget allocation with real-time visualization
|
* Budget Allocator App - Interactive budget allocation with real-time visualization
|
||||||
*/
|
*/
|
||||||
import { App, PostMessageTransport } from "@modelcontextprotocol/ext-apps";
|
import { App } from "@modelcontextprotocol/ext-apps";
|
||||||
import { Chart, registerables } from "chart.js";
|
import { Chart, registerables } from "chart.js";
|
||||||
import "./global.css";
|
import "./global.css";
|
||||||
import "./mcp-app.css";
|
import "./mcp-app.css";
|
||||||
|
|
@ -631,4 +631,4 @@ window
|
||||||
});
|
});
|
||||||
|
|
||||||
// Connect to host
|
// Connect to host
|
||||||
app.connect(new PostMessageTransport(window.parent));
|
app.connect();
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,10 @@ import type { ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
|
import {
|
||||||
|
RESOURCE_MIME_TYPE,
|
||||||
|
RESOURCE_URI_META_KEY,
|
||||||
|
} from "@modelcontextprotocol/ext-apps/server";
|
||||||
import { startServer } from "../shared/server-utils.js";
|
import { startServer } from "../shared/server-utils.js";
|
||||||
|
|
||||||
const DIST_DIR = path.join(import.meta.dirname, "dist");
|
const DIST_DIR = path.join(import.meta.dirname, "dist");
|
||||||
|
|
@ -156,7 +159,8 @@ function createServer(): McpServer {
|
||||||
// Register tool and resource
|
// Register tool and resource
|
||||||
const resourceUri = "ui://get-cohort-data/mcp-app.html";
|
const resourceUri = "ui://get-cohort-data/mcp-app.html";
|
||||||
|
|
||||||
server.registerTool(
|
registerAppTool(
|
||||||
|
server,
|
||||||
"get-cohort-data",
|
"get-cohort-data",
|
||||||
{
|
{
|
||||||
title: "Get Cohort Retention Data",
|
title: "Get Cohort Retention Data",
|
||||||
|
|
@ -179,7 +183,8 @@ function createServer(): McpServer {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
server.registerResource(
|
registerAppResource(
|
||||||
|
server,
|
||||||
resourceUri,
|
resourceUri,
|
||||||
resourceUri,
|
resourceUri,
|
||||||
{ mimeType: RESOURCE_MIME_TYPE },
|
{ mimeType: RESOURCE_MIME_TYPE },
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,10 @@ import type {
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
|
import {
|
||||||
|
RESOURCE_MIME_TYPE,
|
||||||
|
RESOURCE_URI_META_KEY,
|
||||||
|
} from "@modelcontextprotocol/ext-apps/server";
|
||||||
import { startServer } from "../shared/server-utils.js";
|
import { startServer } from "../shared/server-utils.js";
|
||||||
import {
|
import {
|
||||||
generateCustomers,
|
generateCustomers,
|
||||||
|
|
@ -65,7 +68,8 @@ function createServer(): McpServer {
|
||||||
{
|
{
|
||||||
const resourceUri = "ui://customer-segmentation/mcp-app.html";
|
const resourceUri = "ui://customer-segmentation/mcp-app.html";
|
||||||
|
|
||||||
server.registerTool(
|
registerAppTool(
|
||||||
|
server,
|
||||||
"get-customer-data",
|
"get-customer-data",
|
||||||
{
|
{
|
||||||
title: "Get Customer Data",
|
title: "Get Customer Data",
|
||||||
|
|
@ -83,7 +87,8 @@ function createServer(): McpServer {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
server.registerResource(
|
registerAppResource(
|
||||||
|
server,
|
||||||
resourceUri,
|
resourceUri,
|
||||||
resourceUri,
|
resourceUri,
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -463,7 +463,7 @@ app.onhostcontextchanged = (params) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
app.connect(new PostMessageTransport(window.parent)).then(() => {
|
app.connect().then(() => {
|
||||||
// Apply initial host context after connection
|
// Apply initial host context after connection
|
||||||
const ctx = app.getHostContext();
|
const ctx = app.getHostContext();
|
||||||
if (ctx?.theme) {
|
if (ctx?.theme) {
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
await app.connect(new PostMessageTransport(window.parent));
|
await app.connect();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,10 @@ import type {
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
|
import {
|
||||||
|
RESOURCE_MIME_TYPE,
|
||||||
|
RESOURCE_URI_META_KEY,
|
||||||
|
} from "@modelcontextprotocol/ext-apps/server";
|
||||||
import { startServer } from "../shared/server-utils.js";
|
import { startServer } from "../shared/server-utils.js";
|
||||||
|
|
||||||
const DIST_DIR = path.join(import.meta.dirname, "dist");
|
const DIST_DIR = path.join(import.meta.dirname, "dist");
|
||||||
|
|
@ -256,7 +259,8 @@ function createServer(): McpServer {
|
||||||
{
|
{
|
||||||
const resourceUri = "ui://scenario-modeler/mcp-app.html";
|
const resourceUri = "ui://scenario-modeler/mcp-app.html";
|
||||||
|
|
||||||
server.registerTool(
|
registerAppTool(
|
||||||
|
server,
|
||||||
"get-scenario-data",
|
"get-scenario-data",
|
||||||
{
|
{
|
||||||
title: "Get Scenario Data",
|
title: "Get Scenario Data",
|
||||||
|
|
@ -288,7 +292,8 @@ function createServer(): McpServer {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
server.registerResource(
|
registerAppResource(
|
||||||
|
server,
|
||||||
resourceUri,
|
resourceUri,
|
||||||
resourceUri,
|
resourceUri,
|
||||||
{ mimeType: RESOURCE_MIME_TYPE, description: "SaaS Scenario Modeler UI" },
|
{ mimeType: RESOURCE_MIME_TYPE, description: "SaaS Scenario Modeler UI" },
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,10 @@ import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import si from "systeminformation";
|
import si from "systeminformation";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
|
import {
|
||||||
|
RESOURCE_MIME_TYPE,
|
||||||
|
RESOURCE_URI_META_KEY,
|
||||||
|
} from "@modelcontextprotocol/ext-apps/server";
|
||||||
import { startServer } from "../shared/server-utils.js";
|
import { startServer } from "../shared/server-utils.js";
|
||||||
|
|
||||||
// Schemas - types are derived from these using z.infer
|
// Schemas - types are derived from these using z.infer
|
||||||
|
|
@ -111,7 +114,8 @@ function createServer(): McpServer {
|
||||||
// Register the get-system-stats tool and its associated UI resource
|
// Register the get-system-stats tool and its associated UI resource
|
||||||
const resourceUri = "ui://system-monitor/mcp-app.html";
|
const resourceUri = "ui://system-monitor/mcp-app.html";
|
||||||
|
|
||||||
server.registerTool(
|
registerAppTool(
|
||||||
|
server,
|
||||||
"get-system-stats",
|
"get-system-stats",
|
||||||
{
|
{
|
||||||
title: "Get System Stats",
|
title: "Get System Stats",
|
||||||
|
|
@ -149,7 +153,8 @@ function createServer(): McpServer {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
server.registerResource(
|
registerAppResource(
|
||||||
|
server,
|
||||||
resourceUri,
|
resourceUri,
|
||||||
resourceUri,
|
resourceUri,
|
||||||
{ mimeType: RESOURCE_MIME_TYPE, description: "System Monitor UI" },
|
{ mimeType: RESOURCE_MIME_TYPE, description: "System Monitor UI" },
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* @file System Monitor App - displays real-time OS metrics with Chart.js
|
* @file System Monitor App - displays real-time OS metrics with Chart.js
|
||||||
*/
|
*/
|
||||||
import { App, PostMessageTransport } from "@modelcontextprotocol/ext-apps";
|
import { App } from "@modelcontextprotocol/ext-apps";
|
||||||
import { Chart, registerables } from "chart.js";
|
import { Chart, registerables } from "chart.js";
|
||||||
import "./global.css";
|
import "./global.css";
|
||||||
import "./mcp-app.css";
|
import "./mcp-app.css";
|
||||||
|
|
@ -360,7 +360,7 @@ window
|
||||||
// Register handlers and connect
|
// Register handlers and connect
|
||||||
app.onerror = log.error;
|
app.onerror = log.error;
|
||||||
|
|
||||||
app.connect(new PostMessageTransport(window.parent));
|
app.connect();
|
||||||
|
|
||||||
// Auto-start polling after a short delay
|
// Auto-start polling after a short delay
|
||||||
setTimeout(startPolling, 500);
|
setTimeout(startPolling, 500);
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,10 @@ import type { ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
|
import {
|
||||||
|
RESOURCE_MIME_TYPE,
|
||||||
|
RESOURCE_URI_META_KEY,
|
||||||
|
} from "@modelcontextprotocol/ext-apps/server";
|
||||||
import { startServer } from "../shared/server-utils.js";
|
import { startServer } from "../shared/server-utils.js";
|
||||||
|
|
||||||
const DIST_DIR = path.join(import.meta.dirname, "dist");
|
const DIST_DIR = path.join(import.meta.dirname, "dist");
|
||||||
|
|
@ -142,7 +145,8 @@ function createServer(): McpServer {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Tool 1: show_threejs_scene
|
// Tool 1: show_threejs_scene
|
||||||
server.registerTool(
|
registerAppTool(
|
||||||
|
server,
|
||||||
"show_threejs_scene",
|
"show_threejs_scene",
|
||||||
{
|
{
|
||||||
title: "Show Three.js Scene",
|
title: "Show Three.js Scene",
|
||||||
|
|
@ -175,7 +179,8 @@ function createServer(): McpServer {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Tool 2: learn_threejs
|
// Tool 2: learn_threejs
|
||||||
server.registerTool(
|
registerAppTool(
|
||||||
|
server,
|
||||||
"learn_threejs",
|
"learn_threejs",
|
||||||
{
|
{
|
||||||
title: "Learn Three.js",
|
title: "Learn Three.js",
|
||||||
|
|
@ -191,7 +196,8 @@ function createServer(): McpServer {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Resource registration
|
// Resource registration
|
||||||
server.registerResource(
|
registerAppResource(
|
||||||
|
server,
|
||||||
resourceUri,
|
resourceUri,
|
||||||
resourceUri,
|
resourceUri,
|
||||||
{ mimeType: RESOURCE_MIME_TYPE, description: "Three.js Widget UI" },
|
{ mimeType: RESOURCE_MIME_TYPE, description: "Three.js Widget UI" },
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,10 @@ import * as cheerio from "cheerio";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
|
import {
|
||||||
|
RESOURCE_MIME_TYPE,
|
||||||
|
RESOURCE_URI_META_KEY,
|
||||||
|
} from "@modelcontextprotocol/ext-apps/server";
|
||||||
import { startServer } from "../shared/server-utils.js";
|
import { startServer } from "../shared/server-utils.js";
|
||||||
|
|
||||||
const DIST_DIR = path.join(import.meta.dirname, "dist");
|
const DIST_DIR = path.join(import.meta.dirname, "dist");
|
||||||
|
|
@ -76,7 +79,8 @@ function createServer(): McpServer {
|
||||||
// Register the get-first-degree-links tool and its associated UI resource
|
// Register the get-first-degree-links tool and its associated UI resource
|
||||||
const resourceUri = "ui://wiki-explorer/mcp-app.html";
|
const resourceUri = "ui://wiki-explorer/mcp-app.html";
|
||||||
|
|
||||||
server.registerTool(
|
registerAppTool(
|
||||||
|
server,
|
||||||
"get-first-degree-links",
|
"get-first-degree-links",
|
||||||
{
|
{
|
||||||
title: "Get First-Degree Links",
|
title: "Get First-Degree Links",
|
||||||
|
|
@ -124,7 +128,8 @@ function createServer(): McpServer {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
server.registerResource(
|
registerAppResource(
|
||||||
|
server,
|
||||||
resourceUri,
|
resourceUri,
|
||||||
resourceUri,
|
resourceUri,
|
||||||
{ mimeType: RESOURCE_MIME_TYPE },
|
{ mimeType: RESOURCE_MIME_TYPE },
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Wiki Explorer - Force-directed graph visualization of Wikipedia link networks
|
* Wiki Explorer - Force-directed graph visualization of Wikipedia link networks
|
||||||
*/
|
*/
|
||||||
import { App, PostMessageTransport } from "@modelcontextprotocol/ext-apps";
|
import { App } from "@modelcontextprotocol/ext-apps";
|
||||||
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
||||||
import {
|
import {
|
||||||
forceCenter,
|
forceCenter,
|
||||||
|
|
@ -367,4 +367,4 @@ app.onerror = (err) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Connect to host
|
// Connect to host
|
||||||
app.connect(new PostMessageTransport(window.parent));
|
app.connect();
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,10 @@
|
||||||
"types": "./dist/src/app-bridge.d.ts",
|
"types": "./dist/src/app-bridge.d.ts",
|
||||||
"default": "./dist/src/app-bridge.js"
|
"default": "./dist/src/app-bridge.js"
|
||||||
},
|
},
|
||||||
|
"./server": {
|
||||||
|
"types": "./dist/src/server/index.d.ts",
|
||||||
|
"default": "./dist/src/server/index.js"
|
||||||
|
},
|
||||||
"./schema.json": "./dist/src/generated/schema.json"
|
"./schema.json": "./dist/src/generated/schema.json"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import {
|
||||||
PingRequestSchema,
|
PingRequestSchema,
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
import { AppNotification, AppRequest, AppResult } from "./types";
|
import { AppNotification, AppRequest, AppResult } from "./types";
|
||||||
|
import { PostMessageTransport } from "./message-transport";
|
||||||
import {
|
import {
|
||||||
LATEST_PROTOCOL_VERSION,
|
LATEST_PROTOCOL_VERSION,
|
||||||
McpUiAppCapabilities,
|
McpUiAppCapabilities,
|
||||||
|
|
@ -1025,7 +1026,7 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
|
||||||
* @see {@link PostMessageTransport} for the typical transport implementation
|
* @see {@link PostMessageTransport} for the typical transport implementation
|
||||||
*/
|
*/
|
||||||
override async connect(
|
override async connect(
|
||||||
transport: Transport,
|
transport: Transport = new PostMessageTransport(window.parent),
|
||||||
options?: RequestOptions,
|
options?: RequestOptions,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await super.connect(transport);
|
await super.connect(transport);
|
||||||
|
|
|
||||||
179
src/server/index.test.ts
Normal file
179
src/server/index.test.ts
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
import { describe, it, expect, mock } from "bun:test";
|
||||||
|
import {
|
||||||
|
registerAppTool,
|
||||||
|
registerAppResource,
|
||||||
|
RESOURCE_URI_META_KEY,
|
||||||
|
RESOURCE_MIME_TYPE,
|
||||||
|
} from "./index";
|
||||||
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
|
||||||
|
describe("registerAppTool", () => {
|
||||||
|
it("should pass through config to server.registerTool", () => {
|
||||||
|
let capturedName: string | undefined;
|
||||||
|
let capturedConfig: Record<string, unknown> | undefined;
|
||||||
|
let capturedHandler: unknown;
|
||||||
|
|
||||||
|
const mockServer = {
|
||||||
|
registerTool: mock(
|
||||||
|
(name: string, config: Record<string, unknown>, handler: unknown) => {
|
||||||
|
capturedName = name;
|
||||||
|
capturedConfig = config;
|
||||||
|
capturedHandler = handler;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
registerResource: mock(() => {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const handler = async () => ({
|
||||||
|
content: [{ type: "text" as const, text: "ok" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
registerAppTool(
|
||||||
|
mockServer as unknown as Pick<McpServer, "registerTool">,
|
||||||
|
"my-tool",
|
||||||
|
{
|
||||||
|
title: "My Tool",
|
||||||
|
description: "A test tool",
|
||||||
|
_meta: {
|
||||||
|
[RESOURCE_URI_META_KEY]: "ui://test/widget.html",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockServer.registerTool).toHaveBeenCalledTimes(1);
|
||||||
|
expect(capturedName).toBe("my-tool");
|
||||||
|
expect(capturedConfig?.title).toBe("My Tool");
|
||||||
|
expect(capturedConfig?.description).toBe("A test tool");
|
||||||
|
expect(
|
||||||
|
(capturedConfig?._meta as Record<string, unknown>)?.[
|
||||||
|
RESOURCE_URI_META_KEY
|
||||||
|
],
|
||||||
|
).toBe("ui://test/widget.html");
|
||||||
|
expect(capturedHandler).toBe(handler);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("registerAppResource", () => {
|
||||||
|
it("should register a resource with default MIME type", () => {
|
||||||
|
let capturedName: string | undefined;
|
||||||
|
let capturedUri: string | undefined;
|
||||||
|
let capturedConfig: Record<string, unknown> | undefined;
|
||||||
|
|
||||||
|
const mockServer = {
|
||||||
|
registerTool: mock(() => {}),
|
||||||
|
registerResource: mock(
|
||||||
|
(name: string, uri: string, config: Record<string, unknown>) => {
|
||||||
|
capturedName = name;
|
||||||
|
capturedUri = uri;
|
||||||
|
capturedConfig = config;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const callback = async () => ({
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
uri: "ui://test/widget.html",
|
||||||
|
mimeType: RESOURCE_MIME_TYPE,
|
||||||
|
text: "<html/>",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
registerAppResource(
|
||||||
|
mockServer as unknown as Pick<McpServer, "registerResource">,
|
||||||
|
"My Resource",
|
||||||
|
"ui://test/widget.html",
|
||||||
|
{
|
||||||
|
description: "A test resource",
|
||||||
|
_meta: { ui: {} },
|
||||||
|
},
|
||||||
|
callback,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockServer.registerResource).toHaveBeenCalledTimes(1);
|
||||||
|
expect(capturedName).toBe("My Resource");
|
||||||
|
expect(capturedUri).toBe("ui://test/widget.html");
|
||||||
|
expect(capturedConfig?.mimeType).toBe(RESOURCE_MIME_TYPE);
|
||||||
|
expect(capturedConfig?.description).toBe("A test resource");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow custom MIME type to override default", () => {
|
||||||
|
let capturedConfig: Record<string, unknown> | undefined;
|
||||||
|
|
||||||
|
const mockServer = {
|
||||||
|
registerTool: mock(() => {}),
|
||||||
|
registerResource: mock(
|
||||||
|
(_name: string, _uri: string, config: Record<string, unknown>) => {
|
||||||
|
capturedConfig = config;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
registerAppResource(
|
||||||
|
mockServer as unknown as Pick<McpServer, "registerResource">,
|
||||||
|
"My Resource",
|
||||||
|
"ui://test/widget.html",
|
||||||
|
{
|
||||||
|
mimeType: "text/html",
|
||||||
|
_meta: { ui: {} },
|
||||||
|
},
|
||||||
|
async () => ({
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
uri: "ui://test/widget.html",
|
||||||
|
mimeType: "text/html",
|
||||||
|
text: "<html/>",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Custom mimeType should override the default
|
||||||
|
expect(capturedConfig?.mimeType).toBe("text/html");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call the callback when handler is invoked", async () => {
|
||||||
|
let capturedHandler: (() => Promise<unknown>) | undefined;
|
||||||
|
|
||||||
|
const mockServer = {
|
||||||
|
registerTool: mock(() => {}),
|
||||||
|
registerResource: mock(
|
||||||
|
(
|
||||||
|
_name: string,
|
||||||
|
_uri: string,
|
||||||
|
_config: unknown,
|
||||||
|
handler: () => Promise<unknown>,
|
||||||
|
) => {
|
||||||
|
capturedHandler = handler;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectedResult = {
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
uri: "ui://test/widget.html",
|
||||||
|
mimeType: RESOURCE_MIME_TYPE,
|
||||||
|
text: "<html>content</html>",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const callback = mock(async () => expectedResult);
|
||||||
|
|
||||||
|
registerAppResource(
|
||||||
|
mockServer as unknown as Pick<McpServer, "registerResource">,
|
||||||
|
"My Resource",
|
||||||
|
"ui://test/widget.html",
|
||||||
|
{ _meta: { ui: {} } },
|
||||||
|
callback,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(capturedHandler).toBeDefined();
|
||||||
|
const result = await capturedHandler!();
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledTimes(1);
|
||||||
|
expect(result).toEqual(expectedResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
147
src/server/index.ts
Normal file
147
src/server/index.ts
Normal file
|
|
@ -0,0 +1,147 @@
|
||||||
|
/**
|
||||||
|
* Server Helpers for MCP Apps.
|
||||||
|
*
|
||||||
|
* @module server-helpers
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
RESOURCE_URI_META_KEY,
|
||||||
|
RESOURCE_MIME_TYPE,
|
||||||
|
McpUiResourceMeta,
|
||||||
|
} from "../app.js";
|
||||||
|
import type {
|
||||||
|
McpServer,
|
||||||
|
ResourceMetadata,
|
||||||
|
ToolCallback,
|
||||||
|
ReadResourceCallback,
|
||||||
|
} from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
import type { ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
import type { ZodRawShape } from "zod";
|
||||||
|
|
||||||
|
// Re-exports for convenience
|
||||||
|
export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE };
|
||||||
|
export type { ResourceMetadata, ToolCallback, ReadResourceCallback };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool configuration (same as McpServer.registerTool).
|
||||||
|
*/
|
||||||
|
export interface ToolConfig {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
inputSchema?: ZodRawShape;
|
||||||
|
annotations?: ToolAnnotations;
|
||||||
|
_meta?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP App Tool configuration for `registerAppTool`.
|
||||||
|
*/
|
||||||
|
export interface McpUiAppToolConfig extends ToolConfig {
|
||||||
|
_meta: {
|
||||||
|
// Soon: `ui: McpUiToolMeta;` (https://github.com/modelcontextprotocol/ext-apps/pull/131)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URI of the UI resource to display for this tool.
|
||||||
|
* This is converted to `_meta["ui/resourceUri"]`.
|
||||||
|
*
|
||||||
|
* @example "ui://weather/widget.html"
|
||||||
|
*/
|
||||||
|
[RESOURCE_URI_META_KEY]: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP App Resource configuration for `registerAppResource`.
|
||||||
|
*/
|
||||||
|
export interface McpUiAppResourceConfig extends ResourceMetadata {
|
||||||
|
_meta?: {
|
||||||
|
ui?: McpUiResourceMeta;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register an app tool with the MCP server.
|
||||||
|
*
|
||||||
|
* This is a convenience wrapper around `server.registerTool` that will allow more backwards-compatibility.
|
||||||
|
*
|
||||||
|
* @param server - The MCP server instance
|
||||||
|
* @param name - Tool name/identifier
|
||||||
|
* @param config - Tool configuration with required `ui` field
|
||||||
|
* @param handler - Tool handler function
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* import { registerAppTool } from '@modelcontextprotocol/ext-apps/server';
|
||||||
|
* import { z } from 'zod';
|
||||||
|
*
|
||||||
|
* registerAppTool(server, "get-weather", {
|
||||||
|
* title: "Get Weather",
|
||||||
|
* description: "Get current weather for a location",
|
||||||
|
* inputSchema: { location: z.string() },
|
||||||
|
* _meta: {
|
||||||
|
* [RESOURCE_URI_META_KEY]: "ui://weather/widget.html",
|
||||||
|
* },
|
||||||
|
* }, async (args) => {
|
||||||
|
* const weather = await fetchWeather(args.location);
|
||||||
|
* return { content: [{ type: "text", text: JSON.stringify(weather) }] };
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function registerAppTool(
|
||||||
|
server: Pick<McpServer, "registerTool">,
|
||||||
|
name: string,
|
||||||
|
config: McpUiAppToolConfig,
|
||||||
|
handler: ToolCallback<ZodRawShape>,
|
||||||
|
): void {
|
||||||
|
server.registerTool(name, config, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register an app resource with the MCP server.
|
||||||
|
*
|
||||||
|
* This is a convenience wrapper around `server.registerResource` that:
|
||||||
|
* - Defaults the MIME type to "text/html;profile=mcp-app"
|
||||||
|
* - Provides a cleaner API matching the SDK's callback signature
|
||||||
|
*
|
||||||
|
* @param server - The MCP server instance
|
||||||
|
* @param name - Human-readable resource name
|
||||||
|
* @param uri - Resource URI (should match the `ui` field in tool config)
|
||||||
|
* @param config - Resource configuration
|
||||||
|
* @param readCallback - Callback that returns the resource contents
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* import { registerAppResource } from '@modelcontextprotocol/ext-apps/server';
|
||||||
|
*
|
||||||
|
* registerAppResource(server, "Weather Widget", "ui://weather/widget.html", {
|
||||||
|
* description: "Interactive weather display",
|
||||||
|
* mimeType: RESOURCE_MIME_TYPE,
|
||||||
|
* }, async () => ({
|
||||||
|
* contents: [{
|
||||||
|
* uri: "ui://weather/widget.html",
|
||||||
|
* mimeType: RESOURCE_MIME_TYPE,
|
||||||
|
* text: await fs.readFile("dist/widget.html", "utf-8"),
|
||||||
|
* }],
|
||||||
|
* }));
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function registerAppResource(
|
||||||
|
server: Pick<McpServer, "registerResource">,
|
||||||
|
name: string,
|
||||||
|
uri: string,
|
||||||
|
config: McpUiAppResourceConfig,
|
||||||
|
readCallback: ReadResourceCallback,
|
||||||
|
): void {
|
||||||
|
server.registerResource(
|
||||||
|
name,
|
||||||
|
uri,
|
||||||
|
{
|
||||||
|
// Default MIME type for MCP App UI resources (can still be overridden by config below)
|
||||||
|
mimeType: RESOURCE_MIME_TYPE,
|
||||||
|
...config,
|
||||||
|
},
|
||||||
|
readCallback,
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue