Skip to content

Commit db8fd0a

Browse files
ochafikclaude
andcommitted
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 <[email protected]>
1 parent 31e05d3 commit db8fd0a

File tree

7 files changed

+341
-9
lines changed

7 files changed

+341
-9
lines changed

examples/basic-server-react/server.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
33
import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
44
import fs from "node:fs/promises";
55
import path from "node:path";
6-
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
6+
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "@modelcontextprotocol/ext-apps/server";
77
import { startServer } from "../shared/server-utils.js";
88

99
const DIST_DIR = path.join(import.meta.dirname, "dist");
@@ -22,7 +22,7 @@ function createServer(): McpServer {
2222
// MCP Apps require two-part registration: a tool (what the LLM calls) and a
2323
// resource (the UI it renders). The `_meta` field on the tool links to the
2424
// resource URI, telling hosts which UI to display when the tool executes.
25-
server.registerTool(
25+
registerAppTool(server,
2626
"get-time",
2727
{
2828
title: "Get Time",
@@ -38,7 +38,7 @@ function createServer(): McpServer {
3838
},
3939
);
4040

41-
server.registerResource(
41+
registerAppResource(server,
4242
RESOURCE_URI,
4343
RESOURCE_URI,
4444
{ mimeType: RESOURCE_MIME_TYPE },

examples/basic-server-vanillajs/server.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
33
import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
44
import fs from "node:fs/promises";
55
import path from "node:path";
6-
import { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "../../dist/src/app";
6+
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "@modelcontextprotocol/ext-apps/server";
77
import { startServer } from "../shared/server-utils.js";
88

99
const DIST_DIR = path.join(import.meta.dirname, "dist");
@@ -22,7 +22,7 @@ function createServer(): McpServer {
2222
// MCP Apps require two-part registration: a tool (what the LLM calls) and a
2323
// resource (the UI it renders). The `_meta` field on the tool links to the
2424
// resource URI, telling hosts which UI to display when the tool executes.
25-
server.registerTool(
25+
registerAppTool(server,
2626
"get-time",
2727
{
2828
title: "Get Time",
@@ -38,7 +38,7 @@ function createServer(): McpServer {
3838
},
3939
);
4040

41-
server.registerResource(
41+
registerAppResource(server,
4242
RESOURCE_URI,
4343
RESOURCE_URI,
4444
{ mimeType: RESOURCE_MIME_TYPE },

examples/basic-server-vanillajs/src/mcp-app.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* @file App that demonstrates a few features using MCP Apps SDK with vanilla JS.
33
*/
4-
import { App, PostMessageTransport } from "@modelcontextprotocol/ext-apps";
4+
import { App } from "@modelcontextprotocol/ext-apps";
55
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
66
import "./global.css";
77
import "./mcp-app.css";
@@ -98,4 +98,4 @@ openLinkBtn.addEventListener("click", async () => {
9898

9999

100100
// Connect to host
101-
app.connect(new PostMessageTransport(window.parent));
101+
app.connect();

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
"types": "./dist/src/app-bridge.d.ts",
2424
"default": "./dist/src/app-bridge.js"
2525
},
26+
"./server": {
27+
"types": "./dist/src/server/index.d.ts",
28+
"default": "./dist/src/server/index.js"
29+
},
2630
"./schema.json": "./dist/src/generated/schema.json"
2731
},
2832
"files": [

src/app.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
PingRequestSchema,
1717
} from "@modelcontextprotocol/sdk/types.js";
1818
import { AppNotification, AppRequest, AppResult } from "./types";
19+
import { PostMessageTransport } from "./message-transport";
1920
import {
2021
LATEST_PROTOCOL_VERSION,
2122
McpUiAppCapabilities,
@@ -1025,7 +1026,7 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
10251026
* @see {@link PostMessageTransport} for the typical transport implementation
10261027
*/
10271028
override async connect(
1028-
transport: Transport,
1029+
transport: Transport = new PostMessageTransport(window.parent),
10291030
options?: RequestOptions,
10301031
): Promise<void> {
10311032
await super.connect(transport);

src/server/index.test.ts

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import { describe, it, expect, mock } from "bun:test";
2+
import {
3+
registerAppTool,
4+
registerAppResource,
5+
RESOURCE_URI_META_KEY,
6+
RESOURCE_MIME_TYPE,
7+
} from "./index";
8+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9+
10+
describe("registerAppTool", () => {
11+
it("should pass through config to server.registerTool", () => {
12+
let capturedName: string | undefined;
13+
let capturedConfig: Record<string, unknown> | undefined;
14+
let capturedHandler: unknown;
15+
16+
const mockServer = {
17+
registerTool: mock(
18+
(name: string, config: Record<string, unknown>, handler: unknown) => {
19+
capturedName = name;
20+
capturedConfig = config;
21+
capturedHandler = handler;
22+
},
23+
),
24+
registerResource: mock(() => {}),
25+
};
26+
27+
const handler = async () => ({
28+
content: [{ type: "text" as const, text: "ok" }],
29+
});
30+
31+
registerAppTool(
32+
mockServer as unknown as Pick<McpServer, "registerTool">,
33+
"my-tool",
34+
{
35+
title: "My Tool",
36+
description: "A test tool",
37+
_meta: {
38+
[RESOURCE_URI_META_KEY]: "ui://test/widget.html",
39+
},
40+
},
41+
handler,
42+
);
43+
44+
expect(mockServer.registerTool).toHaveBeenCalledTimes(1);
45+
expect(capturedName).toBe("my-tool");
46+
expect(capturedConfig?.title).toBe("My Tool");
47+
expect(capturedConfig?.description).toBe("A test tool");
48+
expect(
49+
(capturedConfig?._meta as Record<string, unknown>)?.[
50+
RESOURCE_URI_META_KEY
51+
],
52+
).toBe("ui://test/widget.html");
53+
expect(capturedHandler).toBe(handler);
54+
});
55+
});
56+
57+
describe("registerAppResource", () => {
58+
it("should register a resource with default MIME type", () => {
59+
let capturedName: string | undefined;
60+
let capturedUri: string | undefined;
61+
let capturedConfig: Record<string, unknown> | undefined;
62+
63+
const mockServer = {
64+
registerTool: mock(() => {}),
65+
registerResource: mock(
66+
(name: string, uri: string, config: Record<string, unknown>) => {
67+
capturedName = name;
68+
capturedUri = uri;
69+
capturedConfig = config;
70+
},
71+
),
72+
};
73+
74+
const callback = async () => ({
75+
contents: [
76+
{
77+
uri: "ui://test/widget.html",
78+
mimeType: RESOURCE_MIME_TYPE,
79+
text: "<html/>",
80+
},
81+
],
82+
});
83+
84+
registerAppResource(
85+
mockServer as unknown as Pick<McpServer, "registerResource">,
86+
"My Resource",
87+
"ui://test/widget.html",
88+
{
89+
description: "A test resource",
90+
_meta: { ui: {} },
91+
},
92+
callback,
93+
);
94+
95+
expect(mockServer.registerResource).toHaveBeenCalledTimes(1);
96+
expect(capturedName).toBe("My Resource");
97+
expect(capturedUri).toBe("ui://test/widget.html");
98+
expect(capturedConfig?.mimeType).toBe(RESOURCE_MIME_TYPE);
99+
expect(capturedConfig?.description).toBe("A test resource");
100+
});
101+
102+
it("should allow custom MIME type to override default", () => {
103+
let capturedConfig: Record<string, unknown> | undefined;
104+
105+
const mockServer = {
106+
registerTool: mock(() => {}),
107+
registerResource: mock(
108+
(_name: string, _uri: string, config: Record<string, unknown>) => {
109+
capturedConfig = config;
110+
},
111+
),
112+
};
113+
114+
registerAppResource(
115+
mockServer as unknown as Pick<McpServer, "registerResource">,
116+
"My Resource",
117+
"ui://test/widget.html",
118+
{
119+
mimeType: "text/html",
120+
_meta: { ui: {} },
121+
},
122+
async () => ({
123+
contents: [
124+
{
125+
uri: "ui://test/widget.html",
126+
mimeType: "text/html",
127+
text: "<html/>",
128+
},
129+
],
130+
}),
131+
);
132+
133+
// Custom mimeType should override the default
134+
expect(capturedConfig?.mimeType).toBe("text/html");
135+
});
136+
137+
it("should call the callback when handler is invoked", async () => {
138+
let capturedHandler: (() => Promise<unknown>) | undefined;
139+
140+
const mockServer = {
141+
registerTool: mock(() => {}),
142+
registerResource: mock(
143+
(
144+
_name: string,
145+
_uri: string,
146+
_config: unknown,
147+
handler: () => Promise<unknown>,
148+
) => {
149+
capturedHandler = handler;
150+
},
151+
),
152+
};
153+
154+
const expectedResult = {
155+
contents: [
156+
{
157+
uri: "ui://test/widget.html",
158+
mimeType: RESOURCE_MIME_TYPE,
159+
text: "<html>content</html>",
160+
},
161+
],
162+
};
163+
const callback = mock(async () => expectedResult);
164+
165+
registerAppResource(
166+
mockServer as unknown as Pick<McpServer, "registerResource">,
167+
"My Resource",
168+
"ui://test/widget.html",
169+
{ _meta: { ui: {} } },
170+
callback,
171+
);
172+
173+
expect(capturedHandler).toBeDefined();
174+
const result = await capturedHandler!();
175+
176+
expect(callback).toHaveBeenCalledTimes(1);
177+
expect(result).toEqual(expectedResult);
178+
});
179+
});

0 commit comments

Comments
 (0)