mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
Load the God's Eye View panel through the MCP server
Hosts serve panels from their own sites and may refuse other addresses; Codex refuses any address on the user's machine. The panel now never requests the app's server: it loads the app's panel build and data through panel_request, a tool only the panel may call, which requests the path from the app's server. One path in every host; only map providers load directly. npm run build:panel writes the panel build to dist/panel, served at /panel/: one app script, one stylesheet, Cesium's script, and Cesium's workers as one script Cesium runs from memory. Tools may now be marked app-only. The panel CORS rules are gone.
This commit is contained in:
1 parent
b11d54d8b8
commit
996b95eb45
20 files changed
+1265
-437
No files matched your search
@@ -274,6 +274,42 @@ test('resources are listed and read, and tools name their UI resource', async ()
|
||||
ui: { resourceUri: 'https://a.example' },
|
||||
run: async () => ({ summary: '', data: {} }),
|
||||
}),
|
||||
/ui:\/\/ resourceUri/,
|
||||
/ui:\/\/ URI/,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
defineTool({
|
||||
name: 'bad',
|
||||
title: 'Bad',
|
||||
description: 'Visible to nobody.',
|
||||
inputSchema: { type: 'object' },
|
||||
ui: { visibility: ['user'] },
|
||||
run: async () => ({ summary: '', data: {} }),
|
||||
}),
|
||||
/visibility/,
|
||||
);
|
||||
});
|
||||
|
||||
test('a tool only an app may call says so, and answers it without JSON text', async () => {
|
||||
const loader = defineTool({
|
||||
name: 'load',
|
||||
title: 'Load',
|
||||
description: 'Loads a file for the view.',
|
||||
inputSchema: { type: 'object', properties: {} },
|
||||
ui: { visibility: ['app'] },
|
||||
run: async () => ({ summary: 'loaded', data: { body: 'abc' } }),
|
||||
});
|
||||
const appServer = createMcpServer({
|
||||
catalog: composeCatalog({ tools: [loader] }),
|
||||
name: 'fixture',
|
||||
version: '1',
|
||||
});
|
||||
const call = async (method, params) =>
|
||||
(await appServer.handle({ jsonrpc: '2.0', id: 1, method, params })).result;
|
||||
assert.deepEqual((await call('tools/list')).tools[0]._meta, {
|
||||
ui: { visibility: ['app'] },
|
||||
});
|
||||
const result = await call('tools/call', { name: 'load', arguments: {} });
|
||||
assert.deepEqual(result.content, [{ type: 'text', text: 'loaded' }]);
|
||||
assert.deepEqual(result.structuredContent, { body: 'abc' });
|
||||
});
|
||||
@@ -41,11 +41,13 @@ export function toMcpTools(catalog, { descriptions = {}, decorate } = {}) {
|
||||
annotations: { title, ...tool.annotations },
|
||||
...(tool.ui
|
||||
? {
|
||||
// The flat key is the earlier form of the same field, which some
|
||||
// hosts still read; the MCP Apps SDK sends both.
|
||||
_meta: {
|
||||
ui: { resourceUri: tool.ui.resourceUri },
|
||||
'ui/resourceUri': tool.ui.resourceUri,
|
||||
ui: { ...tool.ui },
|
||||
// The flat key is the earlier form of the same field, which
|
||||
// some hosts still read; the MCP Apps SDK sends both.
|
||||
...(tool.ui.resourceUri
|
||||
? { 'ui/resourceUri': tool.ui.resourceUri }
|
||||
: {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
@@ -111,12 +113,18 @@ export function createMcpServer({
|
||||
const result = await catalog.call(params.name, params.arguments ?? {}, {
|
||||
signal,
|
||||
});
|
||||
// Only an app reads a tool the model cannot call, and apps read
|
||||
// structuredContent.
|
||||
const appOnly =
|
||||
catalog.get(params.name).ui?.visibility?.includes('model') === false;
|
||||
return {
|
||||
// The data is repeated as JSON text for clients that do not read
|
||||
// structuredContent.
|
||||
content: [
|
||||
{ type: 'text', text: result.summary },
|
||||
{ type: 'text', text: JSON.stringify(result.data) },
|
||||
...(appOnly
|
||||
? []
|
||||
: [{ type: 'text', text: JSON.stringify(result.data) }]),
|
||||
...(result.images || []).map((item) => ({
|
||||
type: 'image',
|
||||
data: item.data,
|
||||
|
||||
Reference in new issue
Block a user