208 lines
5.3 KiB
TypeScript
208 lines
5.3 KiB
TypeScript
import {
|
|
type RequestOptions,
|
|
Protocol,
|
|
ProtocolOptions,
|
|
} from "@modelcontextprotocol/sdk/shared/protocol.js";
|
|
|
|
import {
|
|
CallToolRequest,
|
|
CallToolResult,
|
|
CallToolResultSchema,
|
|
Implementation,
|
|
LoggingMessageNotification,
|
|
Notification,
|
|
PingRequestSchema,
|
|
Request,
|
|
Result,
|
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
import {
|
|
LATEST_PROTOCOL_VERSION,
|
|
McpUiAppCapabilities,
|
|
McpUiHostCapabilities,
|
|
McpUiInitializedNotification,
|
|
McpUiInitializeRequest,
|
|
McpUiInitializeResultSchema,
|
|
McpUiMessageRequest,
|
|
McpUiMessageResultSchema,
|
|
McpUiOpenLinkRequest,
|
|
McpUiOpenLinkResultSchema,
|
|
McpUiSizeChangeNotification,
|
|
} from "./types";
|
|
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
|
|
export { PostMessageTransport } from "./message-transport.js";
|
|
export * from "./types";
|
|
|
|
type AppOptions = ProtocolOptions & {
|
|
autoResize?: boolean;
|
|
};
|
|
|
|
export class App extends Protocol<Request, Notification, Result> {
|
|
private _hostCapabilities?: McpUiHostCapabilities;
|
|
private _hostInfo?: Implementation;
|
|
|
|
constructor(
|
|
private _appInfo: Implementation,
|
|
private _capabilities: McpUiAppCapabilities = {},
|
|
private options: AppOptions = { autoResize: true },
|
|
) {
|
|
super(options);
|
|
|
|
this.setRequestHandler(PingRequestSchema, (request) => {
|
|
console.log("Received ping:", request.params);
|
|
return {};
|
|
});
|
|
}
|
|
|
|
assertCapabilityForMethod(method: Request["method"]): void {
|
|
// TODO
|
|
}
|
|
assertRequestHandlerCapability(method: Request["method"]): void {
|
|
switch (method) {
|
|
case "tools/call":
|
|
case "tools/list":
|
|
if (!this._capabilities.tools) {
|
|
throw new Error(
|
|
`Client does not support tool capability (required for ${method})`,
|
|
);
|
|
}
|
|
return;
|
|
case "ping":
|
|
return;
|
|
default:
|
|
throw new Error(`No handler for method ${method} registered`);
|
|
}
|
|
}
|
|
assertNotificationCapability(method: Notification["method"]): void {
|
|
// TODO
|
|
}
|
|
|
|
async callServerTool(
|
|
params: CallToolRequest["params"],
|
|
options?: RequestOptions,
|
|
): Promise<CallToolResult> {
|
|
return await this.request(
|
|
{ method: "tools/call", params },
|
|
CallToolResultSchema,
|
|
options,
|
|
);
|
|
}
|
|
|
|
sendMessage(params: McpUiMessageRequest["params"], options?: RequestOptions) {
|
|
return this.request(
|
|
<McpUiMessageRequest>{
|
|
method: "ui/message",
|
|
params,
|
|
},
|
|
McpUiMessageResultSchema,
|
|
options,
|
|
);
|
|
}
|
|
|
|
sendLog(params: LoggingMessageNotification["params"]) {
|
|
return this.notification(<LoggingMessageNotification>{
|
|
method: "notifications/message",
|
|
params,
|
|
});
|
|
}
|
|
|
|
sendOpenLink(
|
|
params: McpUiOpenLinkRequest["params"],
|
|
options?: RequestOptions,
|
|
) {
|
|
return this.request(
|
|
<McpUiOpenLinkRequest>{
|
|
method: "ui/open-link",
|
|
params,
|
|
},
|
|
McpUiOpenLinkResultSchema,
|
|
options,
|
|
);
|
|
}
|
|
|
|
sendSizeChange(params: McpUiSizeChangeNotification["params"]) {
|
|
return this.notification(<McpUiSizeChangeNotification>{
|
|
method: "ui/notifications/size-change",
|
|
params,
|
|
});
|
|
}
|
|
|
|
setupSizeChangeNotifications() {
|
|
const sendBodySizeChange = () => {
|
|
let rafId: number | null = null;
|
|
|
|
// Debounce using requestAnimationFrame to avoid duplicate messages
|
|
// when both documentElement and body fire resize events
|
|
if (rafId !== null) {
|
|
cancelAnimationFrame(rafId);
|
|
}
|
|
rafId = requestAnimationFrame(() => {
|
|
const { body, documentElement: html } = document;
|
|
|
|
const bodyStyle = getComputedStyle(body);
|
|
const htmlStyle = getComputedStyle(html);
|
|
|
|
const width = body.scrollWidth;
|
|
const height =
|
|
body.scrollHeight +
|
|
(parseFloat(bodyStyle.borderTop) || 0) +
|
|
(parseFloat(bodyStyle.borderBottom) || 0) +
|
|
(parseFloat(htmlStyle.borderTop) || 0) +
|
|
(parseFloat(htmlStyle.borderBottom) || 0);
|
|
|
|
this.sendSizeChange({ width, height });
|
|
rafId = null;
|
|
});
|
|
};
|
|
|
|
sendBodySizeChange();
|
|
|
|
const resizeObserver = new ResizeObserver(sendBodySizeChange);
|
|
// Observe both html and body to catch all size changes
|
|
resizeObserver.observe(document.documentElement);
|
|
resizeObserver.observe(document.body);
|
|
|
|
return () => resizeObserver.disconnect();
|
|
}
|
|
|
|
override async connect(
|
|
transport: Transport,
|
|
options?: RequestOptions,
|
|
): Promise<void> {
|
|
await super.connect(transport);
|
|
|
|
try {
|
|
const result = await this.request(
|
|
<McpUiInitializeRequest>{
|
|
method: "ui/initialize",
|
|
params: {
|
|
appCapabilities: this._capabilities,
|
|
appInfo: this._appInfo,
|
|
protocolVersion: LATEST_PROTOCOL_VERSION,
|
|
},
|
|
},
|
|
McpUiInitializeResultSchema,
|
|
options,
|
|
);
|
|
|
|
if (result === undefined) {
|
|
throw new Error(`Server sent invalid initialize result: ${result}`);
|
|
}
|
|
|
|
this._hostCapabilities = result.hostCapabilities;
|
|
this._hostInfo = result.hostInfo;
|
|
|
|
await this.notification(<McpUiInitializedNotification>{
|
|
method: "ui/notifications/initialized",
|
|
});
|
|
|
|
if (this.options?.autoResize) {
|
|
this.setupSizeChangeNotifications();
|
|
}
|
|
} catch (error) {
|
|
// Disconnect if initialization fails.
|
|
void this.close();
|
|
throw error;
|
|
}
|
|
}
|
|
}
|