mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
feat: show views as the live globe in MCP Apps clients
show_on_globe replaces open_in_gods_eye_view and names an MCP Apps view, a ui:// panel that loads the app in embed mode and posts each later view to it, so the globe in the conversation changes without reloading. It also takes a view another answer returned. Tools can declare a UI resource, and the MCP adapter serves resources/list and resources/read and lists each tool's resource in _meta.ui.resourceUri.
This commit is contained in:
1 parent
60c40865e9
commit
216a75ac71
17 files changed
+417
-41
No files matched your search
@@ -200,3 +200,79 @@ test('the HTTP transport accepts one JSON message per POST', async () => {
|
||||
assert.equal(get.status, 405);
|
||||
assert.equal(get.headers.get('allow'), 'POST');
|
||||
});
|
||||
|
||||
test('resources are listed and read, and tools name their UI resource', async () => {
|
||||
const shown = defineTool({
|
||||
name: 'show',
|
||||
title: 'Show',
|
||||
description: 'Shows something in a view.',
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
ui: { resourceUri: 'ui://fixture/view' },
|
||||
run: async () => ({ summary: 'shown', data: {} }),
|
||||
});
|
||||
const withUi = createMcpServer({
|
||||
catalog: composeCatalog({ tools: [shown] }),
|
||||
name: 'fixture',
|
||||
version: '1',
|
||||
resources: [
|
||||
{
|
||||
uri: 'ui://fixture/view',
|
||||
name: 'view',
|
||||
mimeType: 'text/html;profile=mcp-app',
|
||||
text: '<!doctype html><p>view</p>',
|
||||
_meta: { ui: { csp: { frameDomains: ['https://a.example'] } } },
|
||||
},
|
||||
],
|
||||
});
|
||||
const call = async (method, params) =>
|
||||
(await withUi.handle({ jsonrpc: '2.0', id: 1, method, params })).result ??
|
||||
(await withUi.handle({ jsonrpc: '2.0', id: 1, method, params })).error;
|
||||
assert.deepEqual((await call('initialize', {})).capabilities, {
|
||||
tools: { listChanged: false },
|
||||
resources: { listChanged: false },
|
||||
});
|
||||
assert.deepEqual((await call('tools/list')).tools[0]._meta, {
|
||||
ui: { resourceUri: 'ui://fixture/view' },
|
||||
});
|
||||
assert.deepEqual((await call('resources/list')).resources, [
|
||||
{
|
||||
uri: 'ui://fixture/view',
|
||||
name: 'view',
|
||||
mimeType: 'text/html;profile=mcp-app',
|
||||
_meta: { ui: { csp: { frameDomains: ['https://a.example'] } } },
|
||||
},
|
||||
]);
|
||||
assert.deepEqual(
|
||||
(await call('resources/read', { uri: 'ui://fixture/view' })).contents,
|
||||
[
|
||||
{
|
||||
uri: 'ui://fixture/view',
|
||||
mimeType: 'text/html;profile=mcp-app',
|
||||
text: '<!doctype html><p>view</p>',
|
||||
_meta: { ui: { csp: { frameDomains: ['https://a.example'] } } },
|
||||
},
|
||||
],
|
||||
);
|
||||
assert.equal(
|
||||
(await call('resources/read', { uri: 'ui://fixture/other' })).code,
|
||||
-32002,
|
||||
);
|
||||
// A server without resources does not advertise them.
|
||||
assert.equal(
|
||||
(await server.handle({ jsonrpc: '2.0', id: 1, method: 'initialize' }))
|
||||
.result.capabilities.resources,
|
||||
undefined,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
defineTool({
|
||||
name: 'bad',
|
||||
title: 'Bad',
|
||||
description: 'Points at a web page.',
|
||||
inputSchema: { type: 'object' },
|
||||
ui: { resourceUri: 'https://a.example' },
|
||||
run: async () => ({ summary: '', data: {} }),
|
||||
}),
|
||||
/ui:\/\/ resourceUri/,
|
||||
);
|
||||
});
|
||||
@@ -1,6 +1,8 @@
|
||||
/**
|
||||
* Model Context Protocol server for a tool catalog: JSON-RPC 2.0 handling of
|
||||
* initialize, ping, tools/list and tools/call. Transports (HTTP, stdio) pass
|
||||
* initialize, ping, tools/list and tools/call, and resources/list and
|
||||
* resources/read for the resources an application supplies, such as MCP Apps
|
||||
* views (`ui://` resources) that tools name in `_meta.ui.resourceUri`. Transports (HTTP, stdio) pass
|
||||
* parsed messages in and send the returned responses out.
|
||||
*/
|
||||
|
||||
@@ -18,6 +20,7 @@ const INVALID_REQUEST = -32600;
|
||||
const METHOD_NOT_FOUND = -32601;
|
||||
const INVALID_PARAMS = -32602;
|
||||
const INTERNAL_ERROR = -32603;
|
||||
const RESOURCE_NOT_FOUND = -32002;
|
||||
|
||||
/**
|
||||
* Describe catalog tools as MCP tool definitions.
|
||||
@@ -36,6 +39,9 @@ export function toMcpTools(catalog, { descriptions = {}, decorate } = {}) {
|
||||
description: override.description ?? tool.description,
|
||||
inputSchema: tool.inputSchema,
|
||||
annotations: { title, ...tool.annotations },
|
||||
...(tool.ui
|
||||
? { _meta: { ui: { resourceUri: tool.ui.resourceUri } } }
|
||||
: {}),
|
||||
};
|
||||
return decorate
|
||||
? { ...definition, ...decorate(definition, tool) }
|
||||
@@ -54,8 +60,12 @@ export function createMcpServer({
|
||||
instructions,
|
||||
descriptions,
|
||||
decorate,
|
||||
resources = [],
|
||||
}) {
|
||||
const tools = () => toMcpTools(catalog, { descriptions, decorate });
|
||||
const resourceByUri = new Map(
|
||||
resources.map((resource) => [resource.uri, resource]),
|
||||
);
|
||||
const methods = {
|
||||
initialize(params) {
|
||||
const requested = params?.protocolVersion;
|
||||
@@ -63,13 +73,28 @@ export function createMcpServer({
|
||||
protocolVersion: MCP_PROTOCOL_VERSIONS.includes(requested)
|
||||
? requested
|
||||
: MCP_PROTOCOL_VERSIONS[0],
|
||||
capabilities: { tools: { listChanged: false } },
|
||||
capabilities: {
|
||||
tools: { listChanged: false },
|
||||
...(resources.length ? { resources: { listChanged: false } } : {}),
|
||||
},
|
||||
serverInfo: { name, version },
|
||||
...(instructions ? { instructions } : {}),
|
||||
};
|
||||
},
|
||||
ping: () => ({}),
|
||||
'tools/list': () => ({ tools: tools() }),
|
||||
'resources/list': () => ({
|
||||
resources: resources.map(({ text, ...entry }) => entry),
|
||||
}),
|
||||
'resources/read'(params) {
|
||||
const resource = resourceByUri.get(params?.uri);
|
||||
if (!resource)
|
||||
throw rpcError(RESOURCE_NOT_FOUND, `Unknown resource: ${params?.uri}`);
|
||||
const { uri, mimeType, text, _meta } = resource;
|
||||
return {
|
||||
contents: [{ uri, mimeType, text, ...(_meta ? { _meta } : {}) }],
|
||||
};
|
||||
},
|
||||
async 'tools/call'(params, { signal }) {
|
||||
if (typeof params?.name !== 'string')
|
||||
throw rpcError(INVALID_PARAMS, 'tools/call needs a tool name');
|
||||
|
||||
Reference in new issue
Block a user