@nimblebrain/synapse
Advanced tools
| 'use strict'; | ||
| var extApps = require('@modelcontextprotocol/ext-apps'); | ||
| // src/connect.ts | ||
| // src/content-parser.ts | ||
| function parseToolResultParams(params) { | ||
| const raw = params ?? {}; | ||
| const structuredContent = raw.structuredContent ?? null; | ||
| if (structuredContent != null) { | ||
| return { content: structuredContent, structuredContent, raw }; | ||
| } | ||
| const rawContent = raw.content; | ||
| if (Array.isArray(rawContent)) { | ||
| const texts = rawContent.filter( | ||
| (block) => block != null && typeof block === "object" && block.type === "text" && typeof block.text === "string" | ||
| ).map((block) => block.text); | ||
| if (texts.length > 0) { | ||
| const joined = texts.join(""); | ||
| try { | ||
| return { content: JSON.parse(joined), structuredContent: null, raw }; | ||
| } catch { | ||
| return { content: joined, structuredContent: null, raw }; | ||
| } | ||
| } | ||
| return { content: rawContent, structuredContent: null, raw }; | ||
| } | ||
| if (typeof rawContent === "string") { | ||
| try { | ||
| return { content: JSON.parse(rawContent), structuredContent: null, raw }; | ||
| } catch { | ||
| return { content: rawContent, structuredContent: null, raw }; | ||
| } | ||
| } | ||
| return { content: rawContent ?? null, structuredContent: null, raw }; | ||
| } | ||
| var EVENT_MAP = { | ||
| "tool-result": extApps.TOOL_RESULT_METHOD, | ||
| "tool-input": extApps.TOOL_INPUT_METHOD, | ||
| "tool-input-partial": extApps.TOOL_INPUT_PARTIAL_METHOD, | ||
| "tool-cancelled": extApps.TOOL_CANCELLED_METHOD, | ||
| "theme-changed": extApps.HOST_CONTEXT_CHANGED_METHOD, | ||
| teardown: extApps.RESOURCE_TEARDOWN_METHOD | ||
| }; | ||
| function resolveEventMethod(name) { | ||
| return EVENT_MAP[name] ?? name; | ||
| } | ||
| // src/resize.ts | ||
| function createResizer(send, autoResize) { | ||
| let destroyed = false; | ||
| let observer = null; | ||
| let rafId = null; | ||
| function measureAndSend() { | ||
| if (destroyed) return; | ||
| const width = document.body.scrollWidth; | ||
| const height = document.body.scrollHeight; | ||
| send("ui/notifications/size-changed", { width, height }); | ||
| } | ||
| function resize(width, height) { | ||
| if (destroyed) return; | ||
| if (width !== void 0 && height !== void 0) { | ||
| send("ui/notifications/size-changed", { width, height }); | ||
| } else { | ||
| measureAndSend(); | ||
| } | ||
| } | ||
| if (autoResize && typeof ResizeObserver !== "undefined") { | ||
| observer = new ResizeObserver(() => { | ||
| if (destroyed) return; | ||
| if (rafId !== null) cancelAnimationFrame(rafId); | ||
| rafId = requestAnimationFrame(() => { | ||
| rafId = null; | ||
| measureAndSend(); | ||
| }); | ||
| }); | ||
| observer.observe(document.body); | ||
| } | ||
| function destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| if (rafId !== null) cancelAnimationFrame(rafId); | ||
| observer?.disconnect(); | ||
| observer = null; | ||
| } | ||
| return { resize, measureAndSend, destroy }; | ||
| } | ||
| // src/result-parser.ts | ||
| function parseToolResult(raw) { | ||
| if (raw == null) { | ||
| return { data: null, isError: false }; | ||
| } | ||
| if (isCallToolResult(raw)) { | ||
| return parseCallToolResult(raw); | ||
| } | ||
| return { data: raw, isError: false }; | ||
| } | ||
| function isCallToolResult(value) { | ||
| if (value === null || typeof value !== "object" || Array.isArray(value)) { | ||
| return false; | ||
| } | ||
| return Array.isArray(value.content); | ||
| } | ||
| function isTextBlock(block) { | ||
| if (block === null || typeof block !== "object" || Array.isArray(block)) { | ||
| return false; | ||
| } | ||
| const obj = block; | ||
| return obj.type === "text" && typeof obj.text === "string"; | ||
| } | ||
| function extractMeta(result) { | ||
| const meta = result._meta; | ||
| if (!meta || typeof meta !== "object" || Array.isArray(meta)) return void 0; | ||
| return { ...meta }; | ||
| } | ||
| function parseCallToolResult(result) { | ||
| const isError = result.isError === true; | ||
| const content = result.content; | ||
| const meta = extractMeta(result); | ||
| if (content.length === 0) { | ||
| return { data: null, isError, content, ...meta && { _meta: meta } }; | ||
| } | ||
| const firstText = content.find(isTextBlock); | ||
| if (!firstText) { | ||
| return { data: content, isError, content, ...meta && { _meta: meta } }; | ||
| } | ||
| try { | ||
| return { data: JSON.parse(firstText.text), isError, content, ...meta && { _meta: meta } }; | ||
| } catch { | ||
| return { data: firstText.text, isError, content, ...meta && { _meta: meta } }; | ||
| } | ||
| } | ||
| // src/transport.ts | ||
| var SynapseTransport = class { | ||
| counter = 0; | ||
| destroyed = false; | ||
| pending = /* @__PURE__ */ new Map(); | ||
| handlers = /* @__PURE__ */ new Map(); | ||
| listener; | ||
| constructor() { | ||
| this.listener = (event) => this.handleMessage(event); | ||
| window.addEventListener("message", this.listener); | ||
| } | ||
| send(method, params) { | ||
| if (this.destroyed) return; | ||
| const msg = { | ||
| jsonrpc: "2.0", | ||
| method, | ||
| ...params !== void 0 && { params } | ||
| }; | ||
| window.parent.postMessage(msg, "*"); | ||
| } | ||
| request(method, params) { | ||
| if (this.destroyed) { | ||
| return Promise.reject(new Error("Transport destroyed")); | ||
| } | ||
| const id = `syn-${++this.counter}`; | ||
| const msg = { | ||
| jsonrpc: "2.0", | ||
| method, | ||
| id, | ||
| ...params !== void 0 && { params } | ||
| }; | ||
| return new Promise((resolve, reject) => { | ||
| this.pending.set(id, { resolve, reject }); | ||
| window.parent.postMessage(msg, "*"); | ||
| }); | ||
| } | ||
| onMessage(method, callback) { | ||
| if (!this.handlers.has(method)) { | ||
| this.handlers.set(method, /* @__PURE__ */ new Set()); | ||
| } | ||
| this.handlers.get(method)?.add(callback); | ||
| return () => { | ||
| const set = this.handlers.get(method); | ||
| if (set) { | ||
| set.delete(callback); | ||
| if (set.size === 0) { | ||
| this.handlers.delete(method); | ||
| } | ||
| } | ||
| }; | ||
| } | ||
| destroy() { | ||
| if (this.destroyed) return; | ||
| this.destroyed = true; | ||
| window.removeEventListener("message", this.listener); | ||
| const error = new Error("Transport destroyed"); | ||
| for (const entry of this.pending.values()) { | ||
| entry.reject(error); | ||
| } | ||
| this.pending.clear(); | ||
| this.handlers.clear(); | ||
| } | ||
| handleMessage(event) { | ||
| if (this.destroyed) return; | ||
| const data = event.data; | ||
| if (!data || data.jsonrpc !== "2.0") return; | ||
| if ("id" in data && data.id && !("method" in data)) { | ||
| const response = data; | ||
| const entry = this.pending.get(response.id); | ||
| if (!entry) return; | ||
| this.pending.delete(response.id); | ||
| if (response.error) { | ||
| const err = new Error(response.error.message); | ||
| err.code = response.error.code; | ||
| err.data = response.error.data; | ||
| entry.reject(err); | ||
| } else { | ||
| entry.resolve(response.result); | ||
| } | ||
| return; | ||
| } | ||
| if ("method" in data && !("id" in data && data.id)) { | ||
| const notification = data; | ||
| const set = this.handlers.get(notification.method); | ||
| if (set) { | ||
| for (const handler of set) { | ||
| handler(notification.params); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }; | ||
| // src/connect.ts | ||
| var READ_RESOURCE_METHOD = "resources/read"; | ||
| async function connect(options) { | ||
| const { name, version, autoResize = false } = options; | ||
| const transport = new SynapseTransport(); | ||
| let destroyed = false; | ||
| let currentTheme = { mode: "light", tokens: {} }; | ||
| let hostInfo = { name: "unknown", version: "unknown" }; | ||
| let toolInfo = null; | ||
| let containerDimensions = null; | ||
| const handlers = /* @__PURE__ */ new Map(); | ||
| const resizer = createResizer((method, params) => transport.send(method, params), autoResize); | ||
| resizer.measureAndSend(); | ||
| const initParams = { | ||
| protocolVersion: extApps.LATEST_PROTOCOL_VERSION, | ||
| appInfo: { name, version }, | ||
| appCapabilities: {} | ||
| }; | ||
| const result = await transport.request( | ||
| extApps.INITIALIZE_METHOD, | ||
| initParams | ||
| ); | ||
| if (result) { | ||
| hostInfo = { | ||
| name: result.hostInfo?.name ?? "unknown", | ||
| version: result.hostInfo?.version ?? "unknown" | ||
| }; | ||
| const ctx = result.hostContext; | ||
| if (ctx) { | ||
| currentTheme = { | ||
| mode: ctx.theme === "dark" ? "dark" : "light", | ||
| tokens: ctx.styles?.variables && typeof ctx.styles.variables === "object" ? ctx.styles.variables : {} | ||
| }; | ||
| if (ctx.toolInfo && typeof ctx.toolInfo === "object") { | ||
| toolInfo = { tool: ctx.toolInfo.tool ?? {} }; | ||
| } | ||
| if (ctx.containerDimensions && typeof ctx.containerDimensions === "object") { | ||
| containerDimensions = ctx.containerDimensions; | ||
| } | ||
| injectCssVariables(ctx.styles?.variables); | ||
| } | ||
| } | ||
| transport.onMessage(extApps.HOST_CONTEXT_CHANGED_METHOD, (params) => { | ||
| if (destroyed || !params) return; | ||
| const ctx = params; | ||
| const mode = ctx.theme === "dark" ? "dark" : "light"; | ||
| const variables = ctx.styles?.variables; | ||
| const tokens = variables && typeof variables === "object" ? variables : currentTheme.tokens; | ||
| currentTheme = { mode, tokens }; | ||
| injectCssVariables(tokens); | ||
| const set = handlers.get(extApps.HOST_CONTEXT_CHANGED_METHOD); | ||
| if (set) { | ||
| for (const handler of set) handler(currentTheme); | ||
| } | ||
| }); | ||
| const subscribedMethods = /* @__PURE__ */ new Set([extApps.HOST_CONTEXT_CHANGED_METHOD]); | ||
| function ensureTransportSub(method) { | ||
| if (subscribedMethods.has(method)) return; | ||
| subscribedMethods.add(method); | ||
| const isToolResult = method === extApps.TOOL_RESULT_METHOD; | ||
| transport.onMessage(method, (params) => { | ||
| if (destroyed) return; | ||
| const set = handlers.get(method); | ||
| if (!set) return; | ||
| for (const handler of set) { | ||
| if (isToolResult) { | ||
| handler(parseToolResultParams(params)); | ||
| } else { | ||
| handler(params); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| if (options.on) { | ||
| for (const [event, handler] of Object.entries(options.on)) { | ||
| if (typeof handler === "function") { | ||
| const method = resolveEventMethod(event); | ||
| if (!handlers.has(method)) handlers.set(method, /* @__PURE__ */ new Set()); | ||
| handlers.get(method)?.add(handler); | ||
| ensureTransportSub(method); | ||
| } | ||
| } | ||
| } | ||
| transport.send(extApps.INITIALIZED_METHOD, {}); | ||
| const app = { | ||
| get theme() { | ||
| return { ...currentTheme }; | ||
| }, | ||
| get hostInfo() { | ||
| return { ...hostInfo }; | ||
| }, | ||
| get toolInfo() { | ||
| return toolInfo; | ||
| }, | ||
| get containerDimensions() { | ||
| return containerDimensions; | ||
| }, | ||
| on(event, handler) { | ||
| const method = resolveEventMethod(event); | ||
| if (!handlers.has(method)) { | ||
| handlers.set(method, /* @__PURE__ */ new Set()); | ||
| } | ||
| handlers.get(method)?.add(handler); | ||
| ensureTransportSub(method); | ||
| return () => { | ||
| const set = handlers.get(method); | ||
| if (set) { | ||
| set.delete(handler); | ||
| if (set.size === 0) handlers.delete(method); | ||
| } | ||
| }; | ||
| }, | ||
| resize(width, height) { | ||
| resizer.resize(width, height); | ||
| }, | ||
| openLink(url) { | ||
| if (destroyed) return; | ||
| const params = { url }; | ||
| transport.request(extApps.OPEN_LINK_METHOD, params).catch(() => { | ||
| }); | ||
| }, | ||
| updateModelContext(state, summary) { | ||
| if (destroyed) return; | ||
| const params = { | ||
| structuredContent: state, | ||
| ...summary !== void 0 && { | ||
| content: [{ type: "text", text: summary }] | ||
| } | ||
| }; | ||
| transport.send("ui/update-model-context", params); | ||
| }, | ||
| async callTool(toolName, args) { | ||
| const params = { | ||
| name: toolName, | ||
| arguments: args ?? {} | ||
| }; | ||
| const raw = await transport.request( | ||
| "tools/call", | ||
| params | ||
| ); | ||
| return parseToolResult(raw); | ||
| }, | ||
| async readServerResource(params) { | ||
| const raw = await transport.request( | ||
| READ_RESOURCE_METHOD, | ||
| params | ||
| ); | ||
| return raw; | ||
| }, | ||
| sendMessage(text, context) { | ||
| if (destroyed) return; | ||
| const textBlock = { | ||
| type: "text", | ||
| text, | ||
| ...context && { _meta: { context } } | ||
| }; | ||
| const params = { | ||
| role: "user", | ||
| content: [textBlock] | ||
| }; | ||
| transport.send(extApps.MESSAGE_METHOD, params); | ||
| }, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| resizer.destroy(); | ||
| handlers.clear(); | ||
| transport.destroy(); | ||
| } | ||
| }; | ||
| return app; | ||
| } | ||
| function injectCssVariables(vars) { | ||
| if (!vars || typeof vars !== "object") return; | ||
| for (const [k, v] of Object.entries(vars)) { | ||
| if (typeof k === "string" && typeof v === "string") { | ||
| document.documentElement.style.setProperty(k, v); | ||
| } | ||
| } | ||
| } | ||
| // src/detection.ts | ||
| var DEFAULT_THEME = { | ||
| mode: "light", | ||
| primaryColor: "#6366f1", | ||
| tokens: {} | ||
| }; | ||
| function detectHost(initResponse) { | ||
| const resp = initResponse; | ||
| const hostName = resp?.hostInfo?.name ?? "unknown"; | ||
| const protocolVersion = resp?.protocolVersion ?? "unknown"; | ||
| return { | ||
| isNimbleBrain: hostName === "nimblebrain", | ||
| serverName: hostName, | ||
| protocolVersion | ||
| }; | ||
| } | ||
| function extractTheme(ctx) { | ||
| if (!ctx) return { ...DEFAULT_THEME }; | ||
| const mode = ctx.theme === "light" || ctx.theme === "dark" ? ctx.theme : DEFAULT_THEME.mode; | ||
| const variables = ctx.styles?.variables; | ||
| const tokens = variables && typeof variables === "object" && !Array.isArray(variables) ? variables : {}; | ||
| return { mode, primaryColor: DEFAULT_THEME.primaryColor, tokens }; | ||
| } | ||
| // src/keyboard.ts | ||
| var KeyboardForwarder = class { | ||
| listener; | ||
| destroyed = false; | ||
| constructor(transport, customKeys) { | ||
| const config = customKeys ?? null; | ||
| this.listener = (event) => { | ||
| if (this.destroyed) return; | ||
| if (this.shouldForward(event, config)) { | ||
| event.preventDefault(); | ||
| transport.send("synapse/keydown", { | ||
| key: event.key, | ||
| ctrlKey: event.ctrlKey, | ||
| metaKey: event.metaKey, | ||
| shiftKey: event.shiftKey, | ||
| altKey: event.altKey | ||
| }); | ||
| } | ||
| }; | ||
| document.addEventListener("keydown", this.listener); | ||
| } | ||
| destroy() { | ||
| if (this.destroyed) return; | ||
| this.destroyed = true; | ||
| document.removeEventListener("keydown", this.listener); | ||
| } | ||
| shouldForward(event, config) { | ||
| if (config && config.length === 0) return false; | ||
| if (config) { | ||
| return config.some( | ||
| (k) => event.key.toLowerCase() === k.key.toLowerCase() && (k.ctrl === void 0 || event.ctrlKey === k.ctrl) && (k.meta === void 0 || event.metaKey === k.meta) && (k.shift === void 0 || event.shiftKey === k.shift) && (k.alt === void 0 || event.altKey === k.alt) | ||
| ); | ||
| } | ||
| if (event.key === "Escape") return true; | ||
| if (event.ctrlKey || event.metaKey) { | ||
| const key = event.key.toLowerCase(); | ||
| if (key === "c" || key === "v" || key === "x" || key === "a") return false; | ||
| return true; | ||
| } | ||
| return false; | ||
| } | ||
| }; | ||
| // src/task-handle.ts | ||
| var TOOLS_CALL_METHOD = "tools/call"; | ||
| var TASKS_GET_METHOD = "tasks/get"; | ||
| var TASKS_RESULT_METHOD = "tasks/result"; | ||
| var TASKS_CANCEL_METHOD = "tasks/cancel"; | ||
| var TASKS_STATUS_NOTIFICATION_METHOD = "notifications/tasks/status"; | ||
| function createTaskStatusRouter(transport) { | ||
| const listeners = /* @__PURE__ */ new Map(); | ||
| const unsub = transport.onMessage(TASKS_STATUS_NOTIFICATION_METHOD, (rawParams) => { | ||
| if (!rawParams) return; | ||
| const params = rawParams; | ||
| const taskId = params.taskId; | ||
| if (typeof taskId !== "string") return; | ||
| const set = listeners.get(taskId); | ||
| if (!set || set.size === 0) return; | ||
| const update = { | ||
| taskId: params.taskId, | ||
| status: params.status, | ||
| ...params.statusMessage !== void 0 && { statusMessage: params.statusMessage } | ||
| }; | ||
| for (const cb of set) cb(update); | ||
| }); | ||
| return { | ||
| subscribe(taskId, cb) { | ||
| let set = listeners.get(taskId); | ||
| if (!set) { | ||
| set = /* @__PURE__ */ new Set(); | ||
| listeners.set(taskId, set); | ||
| } | ||
| set.add(cb); | ||
| return () => { | ||
| const s = listeners.get(taskId); | ||
| if (!s) return; | ||
| s.delete(cb); | ||
| if (s.size === 0) listeners.delete(taskId); | ||
| }; | ||
| }, | ||
| dispose() { | ||
| listeners.clear(); | ||
| unsub(); | ||
| } | ||
| }; | ||
| } | ||
| async function callToolAsTask(deps, toolName, args, options) { | ||
| const hostTasks = deps.getHostTasksCapability(); | ||
| if (!hostTasks?.requests?.tools?.call) { | ||
| throw new Error( | ||
| "callToolAsTask: host did not advertise tasks.requests.tools.call in its capabilities. Per MCP 2025-11-25 \xA7, requestors MUST NOT task-augment a tools/call without matching receiver capability. Fall back to `synapse.callTool`." | ||
| ); | ||
| } | ||
| const taskParam = {}; | ||
| if (options?.ttl !== void 0) taskParam.ttl = options.ttl; | ||
| const crossServer = options?.internal ?? deps.internalApp; | ||
| const callParams = { | ||
| name: toolName, | ||
| arguments: args ?? {}, | ||
| task: taskParam, | ||
| ...crossServer ? { server: deps.appName } : {} | ||
| }; | ||
| const raw = await deps.transport.request( | ||
| TOOLS_CALL_METHOD, | ||
| callParams | ||
| ); | ||
| const createResult = raw; | ||
| const initialTask = createResult?.task; | ||
| if (!initialTask || typeof initialTask !== "object" || typeof initialTask.taskId !== "string") { | ||
| throw new Error( | ||
| "callToolAsTask: receiver returned a response without `task` per CreateTaskResult (expected shape: `{ task: { taskId, status, ... } }`). Receiver may not honor the advertised tasks capability." | ||
| ); | ||
| } | ||
| const taskId = initialTask.taskId; | ||
| const localCallbacks = /* @__PURE__ */ new Map(); | ||
| const handle = { | ||
| task: initialTask, | ||
| async result() { | ||
| const params = { taskId }; | ||
| const rawResult = await deps.transport.request( | ||
| TASKS_RESULT_METHOD, | ||
| params | ||
| ); | ||
| const typed = rawResult; | ||
| return parseToolResult(typed); | ||
| }, | ||
| async refresh() { | ||
| const params = { taskId }; | ||
| const raw2 = await deps.transport.request( | ||
| TASKS_GET_METHOD, | ||
| params | ||
| ); | ||
| return projectTask(raw2); | ||
| }, | ||
| async cancel() { | ||
| const params = { taskId }; | ||
| const raw2 = await deps.transport.request( | ||
| TASKS_CANCEL_METHOD, | ||
| params | ||
| ); | ||
| return projectTask(raw2); | ||
| }, | ||
| onStatus(cb) { | ||
| const existing = localCallbacks.get(cb); | ||
| if (existing) return existing; | ||
| const wireUnsub = deps.router.subscribe(taskId, (update) => { | ||
| const merged = { | ||
| taskId: update.taskId, | ||
| status: update.status, | ||
| ttl: initialTask.ttl, | ||
| createdAt: initialTask.createdAt, | ||
| lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString(), | ||
| ...initialTask.pollInterval !== void 0 && { | ||
| pollInterval: initialTask.pollInterval | ||
| }, | ||
| ...update.statusMessage !== void 0 && { statusMessage: update.statusMessage } | ||
| }; | ||
| cb(merged); | ||
| }); | ||
| const unsub = () => { | ||
| localCallbacks.delete(cb); | ||
| wireUnsub(); | ||
| }; | ||
| localCallbacks.set(cb, unsub); | ||
| return unsub; | ||
| } | ||
| }; | ||
| return handle; | ||
| } | ||
| function projectTask(raw) { | ||
| return { | ||
| taskId: raw.taskId, | ||
| status: raw.status, | ||
| ttl: raw.ttl, | ||
| createdAt: raw.createdAt, | ||
| lastUpdatedAt: raw.lastUpdatedAt, | ||
| ...raw.pollInterval !== void 0 && { pollInterval: raw.pollInterval }, | ||
| ...raw.statusMessage !== void 0 && { statusMessage: raw.statusMessage } | ||
| }; | ||
| } | ||
| // src/core.ts | ||
| var READ_RESOURCE_METHOD2 = "resources/read"; | ||
| function createSynapse(options) { | ||
| const { name, version, internal = false, forwardKeys } = options; | ||
| const transport = new SynapseTransport(); | ||
| let hostInfo = null; | ||
| let currentHostContext = {}; | ||
| let hostTasksCapability = null; | ||
| let destroyed = false; | ||
| const taskStatusRouter = createTaskStatusRouter(transport); | ||
| let stateTimer = null; | ||
| let keyboard = null; | ||
| const appCapabilities = { | ||
| tasks: { | ||
| cancel: {}, | ||
| requests: { tools: { call: {} } } | ||
| } | ||
| }; | ||
| const initParams = { | ||
| protocolVersion: extApps.LATEST_PROTOCOL_VERSION, | ||
| appInfo: { name, version }, | ||
| appCapabilities | ||
| }; | ||
| const ready = transport.request(extApps.INITIALIZE_METHOD, initParams).then((result) => { | ||
| hostInfo = detectHost(result); | ||
| currentHostContext = result?.hostContext ?? {}; | ||
| const initResult = result; | ||
| const rawTasks = initResult?.hostCapabilities?.tasks; | ||
| hostTasksCapability = rawTasks && typeof rawTasks === "object" && !Array.isArray(rawTasks) ? rawTasks : void 0; | ||
| injectCssVariables2(extractTheme(currentHostContext).tokens); | ||
| for (const cb of hostContextCallbacks) cb(currentHostContext); | ||
| transport.send(extApps.INITIALIZED_METHOD, {}); | ||
| keyboard = new KeyboardForwarder(transport, forwardKeys); | ||
| }); | ||
| const unsubHostContext = transport.onMessage(extApps.HOST_CONTEXT_CHANGED_METHOD, (params) => { | ||
| currentHostContext = params ?? {}; | ||
| injectCssVariables2(extractTheme(currentHostContext).tokens); | ||
| for (const cb of hostContextCallbacks) cb(currentHostContext); | ||
| }); | ||
| const hostContextCallbacks = /* @__PURE__ */ new Set(); | ||
| const dataCallbacks = /* @__PURE__ */ new Set(); | ||
| const actionCallbacks = /* @__PURE__ */ new Set(); | ||
| const unsubData = transport.onMessage("synapse/data-changed", (params) => { | ||
| if (!params) return; | ||
| const event = { | ||
| source: "agent", | ||
| server: params.server ?? "", | ||
| tool: params.tool ?? "" | ||
| }; | ||
| for (const cb of dataCallbacks) cb(event); | ||
| }); | ||
| const unsubAction = transport.onMessage("synapse/action", (params) => { | ||
| if (!params || typeof params.type !== "string") return; | ||
| const action = { | ||
| type: params.type, | ||
| payload: params.payload ?? {}, | ||
| requiresConfirmation: params.requiresConfirmation === true, | ||
| label: typeof params.label === "string" ? params.label : void 0 | ||
| }; | ||
| for (const cb of actionCallbacks) cb(action); | ||
| }); | ||
| const isNB = () => hostInfo?.isNimbleBrain === true; | ||
| const synapse = { | ||
| get ready() { | ||
| return ready; | ||
| }, | ||
| get isNimbleBrainHost() { | ||
| return isNB(); | ||
| }, | ||
| get destroyed() { | ||
| return destroyed; | ||
| }, | ||
| async callTool(toolName, args) { | ||
| const params = { | ||
| name: toolName, | ||
| arguments: args ?? {} | ||
| }; | ||
| if (internal) { | ||
| params.server = name; | ||
| } | ||
| const raw = await transport.request("tools/call", params); | ||
| return parseToolResult(raw); | ||
| }, | ||
| async callToolAsTask(toolName, args, options2) { | ||
| return callToolAsTask( | ||
| { | ||
| transport, | ||
| router: taskStatusRouter, | ||
| getHostTasksCapability: () => hostTasksCapability, | ||
| appName: name, | ||
| internalApp: internal | ||
| }, | ||
| toolName, | ||
| args, | ||
| options2 | ||
| ); | ||
| }, | ||
| async readResource(uri) { | ||
| const params = { uri }; | ||
| const raw = await transport.request( | ||
| READ_RESOURCE_METHOD2, | ||
| params | ||
| ); | ||
| return raw; | ||
| }, | ||
| onDataChanged(callback) { | ||
| dataCallbacks.add(callback); | ||
| return () => { | ||
| dataCallbacks.delete(callback); | ||
| }; | ||
| }, | ||
| onAction(callback) { | ||
| actionCallbacks.add(callback); | ||
| return () => { | ||
| actionCallbacks.delete(callback); | ||
| }; | ||
| }, | ||
| getHostContext() { | ||
| return currentHostContext; | ||
| }, | ||
| onHostContextChanged(callback) { | ||
| hostContextCallbacks.add(callback); | ||
| return () => { | ||
| hostContextCallbacks.delete(callback); | ||
| }; | ||
| }, | ||
| getTheme() { | ||
| return extractTheme(currentHostContext); | ||
| }, | ||
| // Selector over `onHostContextChanged`: only fires when the *derived* | ||
| // theme actually changes, so theme subscribers don't see spurious | ||
| // updates when other host-context fields (e.g. workspace) change. | ||
| // | ||
| // Subscriber timing matters: | ||
| // - Subscribed BEFORE handshake: `prev = null` sentinel. The first | ||
| // fire (the handshake dispatch) always invokes the callback, | ||
| // even if the host's theme happens to derive to the default. | ||
| // Otherwise consumers using `onThemeChanged` as their init | ||
| // signal would silently miss it. | ||
| // - Subscribed AFTER handshake: `prev` is pre-seeded with the | ||
| // current derived theme, so a workspace-only `host-context-changed` | ||
| // notification correctly filters as a no-op. | ||
| onThemeChanged(callback) { | ||
| let prev = hostInfo !== null ? extractTheme(currentHostContext) : null; | ||
| const wrapped = (ctx) => { | ||
| const next = extractTheme(ctx); | ||
| if (prev !== null && themesEqual(prev, next)) return; | ||
| prev = next; | ||
| callback(next); | ||
| }; | ||
| hostContextCallbacks.add(wrapped); | ||
| return () => { | ||
| hostContextCallbacks.delete(wrapped); | ||
| }; | ||
| }, | ||
| action(action, params) { | ||
| if (!isNB()) return; | ||
| transport.send("synapse/action", { action, ...params }); | ||
| }, | ||
| chat(message, context) { | ||
| const textBlock = { | ||
| type: "text", | ||
| text: message, | ||
| ...isNB() && context && { _meta: { context } } | ||
| }; | ||
| const params = { | ||
| role: "user", | ||
| content: [textBlock] | ||
| }; | ||
| transport.send(extApps.MESSAGE_METHOD, params); | ||
| }, | ||
| setVisibleState(state, summary) { | ||
| if (stateTimer) clearTimeout(stateTimer); | ||
| stateTimer = setTimeout(() => { | ||
| const params = { | ||
| structuredContent: state, | ||
| ...summary !== void 0 && { | ||
| content: [{ type: "text", text: summary }] | ||
| } | ||
| }; | ||
| transport.send("ui/update-model-context", params); | ||
| stateTimer = null; | ||
| }, 250); | ||
| }, | ||
| downloadFile(filename, content, mimeType) { | ||
| const resolvedMime = mimeType || (content instanceof Blob ? content.type : "") || "application/octet-stream"; | ||
| const blob = content instanceof Blob ? content : new Blob([content], { type: resolvedMime }); | ||
| transport.send("synapse/download-file", { | ||
| data: blob, | ||
| filename, | ||
| mimeType: resolvedMime | ||
| }); | ||
| }, | ||
| openLink(url) { | ||
| const params = { url }; | ||
| transport.request(extApps.OPEN_LINK_METHOD, params).catch(() => { | ||
| window.open(url, "_blank", "noopener"); | ||
| }); | ||
| }, | ||
| async pickFile(options2) { | ||
| if (!isNB()) { | ||
| throw new Error("pickFile is not supported in this host"); | ||
| } | ||
| const result = await requestFile(transport, options2, false); | ||
| if (result === null) return null; | ||
| return Array.isArray(result) ? result[0] ?? null : result; | ||
| }, | ||
| async pickFiles(options2) { | ||
| if (!isNB()) { | ||
| throw new Error("pickFiles is not supported in this host"); | ||
| } | ||
| const result = await requestFile(transport, options2, true); | ||
| if (result === null) return []; | ||
| return Array.isArray(result) ? result : [result]; | ||
| }, | ||
| _onMessage(method, callback) { | ||
| return transport.onMessage(method, callback); | ||
| }, | ||
| _request(method, params) { | ||
| return transport.request(method, params); | ||
| }, | ||
| get _hostTasksCapability() { | ||
| return hostTasksCapability; | ||
| }, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| if (stateTimer) clearTimeout(stateTimer); | ||
| keyboard?.destroy(); | ||
| unsubHostContext(); | ||
| unsubData(); | ||
| unsubAction(); | ||
| taskStatusRouter.dispose(); | ||
| hostContextCallbacks.clear(); | ||
| dataCallbacks.clear(); | ||
| actionCallbacks.clear(); | ||
| transport.destroy(); | ||
| } | ||
| }; | ||
| return synapse; | ||
| } | ||
| async function requestFile(transport, options, multiple) { | ||
| const result = await transport.request("synapse/request-file", { | ||
| accept: options?.accept, | ||
| maxSize: options?.maxSize ?? 26214400, | ||
| multiple | ||
| }); | ||
| if (result == null) return null; | ||
| if (Array.isArray(result)) { | ||
| return result.map(validateFileResult); | ||
| } | ||
| return validateFileResult(result); | ||
| } | ||
| function validateFileResult(value) { | ||
| if (typeof value !== "object" || value === null || typeof value.id !== "string") { | ||
| throw new Error( | ||
| "synapse/request-file returned a result without a string `id`. The host appears to be on a version older than this SDK targets \u2014 @nimblebrain/synapse 0.8.0+ requires a host with POST /v1/resources (NimbleBrain \u2265 the version that ships PR #93)." | ||
| ); | ||
| } | ||
| return value; | ||
| } | ||
| function themesEqual(a, b) { | ||
| if (a.mode !== b.mode || a.primaryColor !== b.primaryColor) return false; | ||
| const aKeys = Object.keys(a.tokens); | ||
| const bKeys = Object.keys(b.tokens); | ||
| if (aKeys.length !== bKeys.length) return false; | ||
| for (const k of aKeys) { | ||
| if (a.tokens[k] !== b.tokens[k]) return false; | ||
| } | ||
| return true; | ||
| } | ||
| function injectCssVariables2(vars) { | ||
| if (!vars || typeof vars !== "object") return; | ||
| if (typeof document === "undefined") return; | ||
| for (const [k, v] of Object.entries(vars)) { | ||
| if (typeof k === "string" && typeof v === "string") { | ||
| document.documentElement.style.setProperty(k, v); | ||
| } | ||
| } | ||
| } | ||
| exports.connect = connect; | ||
| exports.createSynapse = createSynapse; | ||
| //# sourceMappingURL=chunk-42JAT6TK.cjs.map | ||
| //# sourceMappingURL=chunk-42JAT6TK.cjs.map |
Sorry, the diff of this file is too big to display
| import { INITIALIZE_METHOD, LATEST_PROTOCOL_VERSION, HOST_CONTEXT_CHANGED_METHOD, INITIALIZED_METHOD, MESSAGE_METHOD, OPEN_LINK_METHOD, TOOL_RESULT_METHOD, RESOURCE_TEARDOWN_METHOD, TOOL_CANCELLED_METHOD, TOOL_INPUT_PARTIAL_METHOD, TOOL_INPUT_METHOD } from '@modelcontextprotocol/ext-apps'; | ||
| // src/connect.ts | ||
| // src/content-parser.ts | ||
| function parseToolResultParams(params) { | ||
| const raw = params ?? {}; | ||
| const structuredContent = raw.structuredContent ?? null; | ||
| if (structuredContent != null) { | ||
| return { content: structuredContent, structuredContent, raw }; | ||
| } | ||
| const rawContent = raw.content; | ||
| if (Array.isArray(rawContent)) { | ||
| const texts = rawContent.filter( | ||
| (block) => block != null && typeof block === "object" && block.type === "text" && typeof block.text === "string" | ||
| ).map((block) => block.text); | ||
| if (texts.length > 0) { | ||
| const joined = texts.join(""); | ||
| try { | ||
| return { content: JSON.parse(joined), structuredContent: null, raw }; | ||
| } catch { | ||
| return { content: joined, structuredContent: null, raw }; | ||
| } | ||
| } | ||
| return { content: rawContent, structuredContent: null, raw }; | ||
| } | ||
| if (typeof rawContent === "string") { | ||
| try { | ||
| return { content: JSON.parse(rawContent), structuredContent: null, raw }; | ||
| } catch { | ||
| return { content: rawContent, structuredContent: null, raw }; | ||
| } | ||
| } | ||
| return { content: rawContent ?? null, structuredContent: null, raw }; | ||
| } | ||
| var EVENT_MAP = { | ||
| "tool-result": TOOL_RESULT_METHOD, | ||
| "tool-input": TOOL_INPUT_METHOD, | ||
| "tool-input-partial": TOOL_INPUT_PARTIAL_METHOD, | ||
| "tool-cancelled": TOOL_CANCELLED_METHOD, | ||
| "theme-changed": HOST_CONTEXT_CHANGED_METHOD, | ||
| teardown: RESOURCE_TEARDOWN_METHOD | ||
| }; | ||
| function resolveEventMethod(name) { | ||
| return EVENT_MAP[name] ?? name; | ||
| } | ||
| // src/resize.ts | ||
| function createResizer(send, autoResize) { | ||
| let destroyed = false; | ||
| let observer = null; | ||
| let rafId = null; | ||
| function measureAndSend() { | ||
| if (destroyed) return; | ||
| const width = document.body.scrollWidth; | ||
| const height = document.body.scrollHeight; | ||
| send("ui/notifications/size-changed", { width, height }); | ||
| } | ||
| function resize(width, height) { | ||
| if (destroyed) return; | ||
| if (width !== void 0 && height !== void 0) { | ||
| send("ui/notifications/size-changed", { width, height }); | ||
| } else { | ||
| measureAndSend(); | ||
| } | ||
| } | ||
| if (autoResize && typeof ResizeObserver !== "undefined") { | ||
| observer = new ResizeObserver(() => { | ||
| if (destroyed) return; | ||
| if (rafId !== null) cancelAnimationFrame(rafId); | ||
| rafId = requestAnimationFrame(() => { | ||
| rafId = null; | ||
| measureAndSend(); | ||
| }); | ||
| }); | ||
| observer.observe(document.body); | ||
| } | ||
| function destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| if (rafId !== null) cancelAnimationFrame(rafId); | ||
| observer?.disconnect(); | ||
| observer = null; | ||
| } | ||
| return { resize, measureAndSend, destroy }; | ||
| } | ||
| // src/result-parser.ts | ||
| function parseToolResult(raw) { | ||
| if (raw == null) { | ||
| return { data: null, isError: false }; | ||
| } | ||
| if (isCallToolResult(raw)) { | ||
| return parseCallToolResult(raw); | ||
| } | ||
| return { data: raw, isError: false }; | ||
| } | ||
| function isCallToolResult(value) { | ||
| if (value === null || typeof value !== "object" || Array.isArray(value)) { | ||
| return false; | ||
| } | ||
| return Array.isArray(value.content); | ||
| } | ||
| function isTextBlock(block) { | ||
| if (block === null || typeof block !== "object" || Array.isArray(block)) { | ||
| return false; | ||
| } | ||
| const obj = block; | ||
| return obj.type === "text" && typeof obj.text === "string"; | ||
| } | ||
| function extractMeta(result) { | ||
| const meta = result._meta; | ||
| if (!meta || typeof meta !== "object" || Array.isArray(meta)) return void 0; | ||
| return { ...meta }; | ||
| } | ||
| function parseCallToolResult(result) { | ||
| const isError = result.isError === true; | ||
| const content = result.content; | ||
| const meta = extractMeta(result); | ||
| if (content.length === 0) { | ||
| return { data: null, isError, content, ...meta && { _meta: meta } }; | ||
| } | ||
| const firstText = content.find(isTextBlock); | ||
| if (!firstText) { | ||
| return { data: content, isError, content, ...meta && { _meta: meta } }; | ||
| } | ||
| try { | ||
| return { data: JSON.parse(firstText.text), isError, content, ...meta && { _meta: meta } }; | ||
| } catch { | ||
| return { data: firstText.text, isError, content, ...meta && { _meta: meta } }; | ||
| } | ||
| } | ||
| // src/transport.ts | ||
| var SynapseTransport = class { | ||
| counter = 0; | ||
| destroyed = false; | ||
| pending = /* @__PURE__ */ new Map(); | ||
| handlers = /* @__PURE__ */ new Map(); | ||
| listener; | ||
| constructor() { | ||
| this.listener = (event) => this.handleMessage(event); | ||
| window.addEventListener("message", this.listener); | ||
| } | ||
| send(method, params) { | ||
| if (this.destroyed) return; | ||
| const msg = { | ||
| jsonrpc: "2.0", | ||
| method, | ||
| ...params !== void 0 && { params } | ||
| }; | ||
| window.parent.postMessage(msg, "*"); | ||
| } | ||
| request(method, params) { | ||
| if (this.destroyed) { | ||
| return Promise.reject(new Error("Transport destroyed")); | ||
| } | ||
| const id = `syn-${++this.counter}`; | ||
| const msg = { | ||
| jsonrpc: "2.0", | ||
| method, | ||
| id, | ||
| ...params !== void 0 && { params } | ||
| }; | ||
| return new Promise((resolve, reject) => { | ||
| this.pending.set(id, { resolve, reject }); | ||
| window.parent.postMessage(msg, "*"); | ||
| }); | ||
| } | ||
| onMessage(method, callback) { | ||
| if (!this.handlers.has(method)) { | ||
| this.handlers.set(method, /* @__PURE__ */ new Set()); | ||
| } | ||
| this.handlers.get(method)?.add(callback); | ||
| return () => { | ||
| const set = this.handlers.get(method); | ||
| if (set) { | ||
| set.delete(callback); | ||
| if (set.size === 0) { | ||
| this.handlers.delete(method); | ||
| } | ||
| } | ||
| }; | ||
| } | ||
| destroy() { | ||
| if (this.destroyed) return; | ||
| this.destroyed = true; | ||
| window.removeEventListener("message", this.listener); | ||
| const error = new Error("Transport destroyed"); | ||
| for (const entry of this.pending.values()) { | ||
| entry.reject(error); | ||
| } | ||
| this.pending.clear(); | ||
| this.handlers.clear(); | ||
| } | ||
| handleMessage(event) { | ||
| if (this.destroyed) return; | ||
| const data = event.data; | ||
| if (!data || data.jsonrpc !== "2.0") return; | ||
| if ("id" in data && data.id && !("method" in data)) { | ||
| const response = data; | ||
| const entry = this.pending.get(response.id); | ||
| if (!entry) return; | ||
| this.pending.delete(response.id); | ||
| if (response.error) { | ||
| const err = new Error(response.error.message); | ||
| err.code = response.error.code; | ||
| err.data = response.error.data; | ||
| entry.reject(err); | ||
| } else { | ||
| entry.resolve(response.result); | ||
| } | ||
| return; | ||
| } | ||
| if ("method" in data && !("id" in data && data.id)) { | ||
| const notification = data; | ||
| const set = this.handlers.get(notification.method); | ||
| if (set) { | ||
| for (const handler of set) { | ||
| handler(notification.params); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }; | ||
| // src/connect.ts | ||
| var READ_RESOURCE_METHOD = "resources/read"; | ||
| async function connect(options) { | ||
| const { name, version, autoResize = false } = options; | ||
| const transport = new SynapseTransport(); | ||
| let destroyed = false; | ||
| let currentTheme = { mode: "light", tokens: {} }; | ||
| let hostInfo = { name: "unknown", version: "unknown" }; | ||
| let toolInfo = null; | ||
| let containerDimensions = null; | ||
| const handlers = /* @__PURE__ */ new Map(); | ||
| const resizer = createResizer((method, params) => transport.send(method, params), autoResize); | ||
| resizer.measureAndSend(); | ||
| const initParams = { | ||
| protocolVersion: LATEST_PROTOCOL_VERSION, | ||
| appInfo: { name, version }, | ||
| appCapabilities: {} | ||
| }; | ||
| const result = await transport.request( | ||
| INITIALIZE_METHOD, | ||
| initParams | ||
| ); | ||
| if (result) { | ||
| hostInfo = { | ||
| name: result.hostInfo?.name ?? "unknown", | ||
| version: result.hostInfo?.version ?? "unknown" | ||
| }; | ||
| const ctx = result.hostContext; | ||
| if (ctx) { | ||
| currentTheme = { | ||
| mode: ctx.theme === "dark" ? "dark" : "light", | ||
| tokens: ctx.styles?.variables && typeof ctx.styles.variables === "object" ? ctx.styles.variables : {} | ||
| }; | ||
| if (ctx.toolInfo && typeof ctx.toolInfo === "object") { | ||
| toolInfo = { tool: ctx.toolInfo.tool ?? {} }; | ||
| } | ||
| if (ctx.containerDimensions && typeof ctx.containerDimensions === "object") { | ||
| containerDimensions = ctx.containerDimensions; | ||
| } | ||
| injectCssVariables(ctx.styles?.variables); | ||
| } | ||
| } | ||
| transport.onMessage(HOST_CONTEXT_CHANGED_METHOD, (params) => { | ||
| if (destroyed || !params) return; | ||
| const ctx = params; | ||
| const mode = ctx.theme === "dark" ? "dark" : "light"; | ||
| const variables = ctx.styles?.variables; | ||
| const tokens = variables && typeof variables === "object" ? variables : currentTheme.tokens; | ||
| currentTheme = { mode, tokens }; | ||
| injectCssVariables(tokens); | ||
| const set = handlers.get(HOST_CONTEXT_CHANGED_METHOD); | ||
| if (set) { | ||
| for (const handler of set) handler(currentTheme); | ||
| } | ||
| }); | ||
| const subscribedMethods = /* @__PURE__ */ new Set([HOST_CONTEXT_CHANGED_METHOD]); | ||
| function ensureTransportSub(method) { | ||
| if (subscribedMethods.has(method)) return; | ||
| subscribedMethods.add(method); | ||
| const isToolResult = method === TOOL_RESULT_METHOD; | ||
| transport.onMessage(method, (params) => { | ||
| if (destroyed) return; | ||
| const set = handlers.get(method); | ||
| if (!set) return; | ||
| for (const handler of set) { | ||
| if (isToolResult) { | ||
| handler(parseToolResultParams(params)); | ||
| } else { | ||
| handler(params); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| if (options.on) { | ||
| for (const [event, handler] of Object.entries(options.on)) { | ||
| if (typeof handler === "function") { | ||
| const method = resolveEventMethod(event); | ||
| if (!handlers.has(method)) handlers.set(method, /* @__PURE__ */ new Set()); | ||
| handlers.get(method)?.add(handler); | ||
| ensureTransportSub(method); | ||
| } | ||
| } | ||
| } | ||
| transport.send(INITIALIZED_METHOD, {}); | ||
| const app = { | ||
| get theme() { | ||
| return { ...currentTheme }; | ||
| }, | ||
| get hostInfo() { | ||
| return { ...hostInfo }; | ||
| }, | ||
| get toolInfo() { | ||
| return toolInfo; | ||
| }, | ||
| get containerDimensions() { | ||
| return containerDimensions; | ||
| }, | ||
| on(event, handler) { | ||
| const method = resolveEventMethod(event); | ||
| if (!handlers.has(method)) { | ||
| handlers.set(method, /* @__PURE__ */ new Set()); | ||
| } | ||
| handlers.get(method)?.add(handler); | ||
| ensureTransportSub(method); | ||
| return () => { | ||
| const set = handlers.get(method); | ||
| if (set) { | ||
| set.delete(handler); | ||
| if (set.size === 0) handlers.delete(method); | ||
| } | ||
| }; | ||
| }, | ||
| resize(width, height) { | ||
| resizer.resize(width, height); | ||
| }, | ||
| openLink(url) { | ||
| if (destroyed) return; | ||
| const params = { url }; | ||
| transport.request(OPEN_LINK_METHOD, params).catch(() => { | ||
| }); | ||
| }, | ||
| updateModelContext(state, summary) { | ||
| if (destroyed) return; | ||
| const params = { | ||
| structuredContent: state, | ||
| ...summary !== void 0 && { | ||
| content: [{ type: "text", text: summary }] | ||
| } | ||
| }; | ||
| transport.send("ui/update-model-context", params); | ||
| }, | ||
| async callTool(toolName, args) { | ||
| const params = { | ||
| name: toolName, | ||
| arguments: args ?? {} | ||
| }; | ||
| const raw = await transport.request( | ||
| "tools/call", | ||
| params | ||
| ); | ||
| return parseToolResult(raw); | ||
| }, | ||
| async readServerResource(params) { | ||
| const raw = await transport.request( | ||
| READ_RESOURCE_METHOD, | ||
| params | ||
| ); | ||
| return raw; | ||
| }, | ||
| sendMessage(text, context) { | ||
| if (destroyed) return; | ||
| const textBlock = { | ||
| type: "text", | ||
| text, | ||
| ...context && { _meta: { context } } | ||
| }; | ||
| const params = { | ||
| role: "user", | ||
| content: [textBlock] | ||
| }; | ||
| transport.send(MESSAGE_METHOD, params); | ||
| }, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| resizer.destroy(); | ||
| handlers.clear(); | ||
| transport.destroy(); | ||
| } | ||
| }; | ||
| return app; | ||
| } | ||
| function injectCssVariables(vars) { | ||
| if (!vars || typeof vars !== "object") return; | ||
| for (const [k, v] of Object.entries(vars)) { | ||
| if (typeof k === "string" && typeof v === "string") { | ||
| document.documentElement.style.setProperty(k, v); | ||
| } | ||
| } | ||
| } | ||
| // src/detection.ts | ||
| var DEFAULT_THEME = { | ||
| mode: "light", | ||
| primaryColor: "#6366f1", | ||
| tokens: {} | ||
| }; | ||
| function detectHost(initResponse) { | ||
| const resp = initResponse; | ||
| const hostName = resp?.hostInfo?.name ?? "unknown"; | ||
| const protocolVersion = resp?.protocolVersion ?? "unknown"; | ||
| return { | ||
| isNimbleBrain: hostName === "nimblebrain", | ||
| serverName: hostName, | ||
| protocolVersion | ||
| }; | ||
| } | ||
| function extractTheme(ctx) { | ||
| if (!ctx) return { ...DEFAULT_THEME }; | ||
| const mode = ctx.theme === "light" || ctx.theme === "dark" ? ctx.theme : DEFAULT_THEME.mode; | ||
| const variables = ctx.styles?.variables; | ||
| const tokens = variables && typeof variables === "object" && !Array.isArray(variables) ? variables : {}; | ||
| return { mode, primaryColor: DEFAULT_THEME.primaryColor, tokens }; | ||
| } | ||
| // src/keyboard.ts | ||
| var KeyboardForwarder = class { | ||
| listener; | ||
| destroyed = false; | ||
| constructor(transport, customKeys) { | ||
| const config = customKeys ?? null; | ||
| this.listener = (event) => { | ||
| if (this.destroyed) return; | ||
| if (this.shouldForward(event, config)) { | ||
| event.preventDefault(); | ||
| transport.send("synapse/keydown", { | ||
| key: event.key, | ||
| ctrlKey: event.ctrlKey, | ||
| metaKey: event.metaKey, | ||
| shiftKey: event.shiftKey, | ||
| altKey: event.altKey | ||
| }); | ||
| } | ||
| }; | ||
| document.addEventListener("keydown", this.listener); | ||
| } | ||
| destroy() { | ||
| if (this.destroyed) return; | ||
| this.destroyed = true; | ||
| document.removeEventListener("keydown", this.listener); | ||
| } | ||
| shouldForward(event, config) { | ||
| if (config && config.length === 0) return false; | ||
| if (config) { | ||
| return config.some( | ||
| (k) => event.key.toLowerCase() === k.key.toLowerCase() && (k.ctrl === void 0 || event.ctrlKey === k.ctrl) && (k.meta === void 0 || event.metaKey === k.meta) && (k.shift === void 0 || event.shiftKey === k.shift) && (k.alt === void 0 || event.altKey === k.alt) | ||
| ); | ||
| } | ||
| if (event.key === "Escape") return true; | ||
| if (event.ctrlKey || event.metaKey) { | ||
| const key = event.key.toLowerCase(); | ||
| if (key === "c" || key === "v" || key === "x" || key === "a") return false; | ||
| return true; | ||
| } | ||
| return false; | ||
| } | ||
| }; | ||
| // src/task-handle.ts | ||
| var TOOLS_CALL_METHOD = "tools/call"; | ||
| var TASKS_GET_METHOD = "tasks/get"; | ||
| var TASKS_RESULT_METHOD = "tasks/result"; | ||
| var TASKS_CANCEL_METHOD = "tasks/cancel"; | ||
| var TASKS_STATUS_NOTIFICATION_METHOD = "notifications/tasks/status"; | ||
| function createTaskStatusRouter(transport) { | ||
| const listeners = /* @__PURE__ */ new Map(); | ||
| const unsub = transport.onMessage(TASKS_STATUS_NOTIFICATION_METHOD, (rawParams) => { | ||
| if (!rawParams) return; | ||
| const params = rawParams; | ||
| const taskId = params.taskId; | ||
| if (typeof taskId !== "string") return; | ||
| const set = listeners.get(taskId); | ||
| if (!set || set.size === 0) return; | ||
| const update = { | ||
| taskId: params.taskId, | ||
| status: params.status, | ||
| ...params.statusMessage !== void 0 && { statusMessage: params.statusMessage } | ||
| }; | ||
| for (const cb of set) cb(update); | ||
| }); | ||
| return { | ||
| subscribe(taskId, cb) { | ||
| let set = listeners.get(taskId); | ||
| if (!set) { | ||
| set = /* @__PURE__ */ new Set(); | ||
| listeners.set(taskId, set); | ||
| } | ||
| set.add(cb); | ||
| return () => { | ||
| const s = listeners.get(taskId); | ||
| if (!s) return; | ||
| s.delete(cb); | ||
| if (s.size === 0) listeners.delete(taskId); | ||
| }; | ||
| }, | ||
| dispose() { | ||
| listeners.clear(); | ||
| unsub(); | ||
| } | ||
| }; | ||
| } | ||
| async function callToolAsTask(deps, toolName, args, options) { | ||
| const hostTasks = deps.getHostTasksCapability(); | ||
| if (!hostTasks?.requests?.tools?.call) { | ||
| throw new Error( | ||
| "callToolAsTask: host did not advertise tasks.requests.tools.call in its capabilities. Per MCP 2025-11-25 \xA7, requestors MUST NOT task-augment a tools/call without matching receiver capability. Fall back to `synapse.callTool`." | ||
| ); | ||
| } | ||
| const taskParam = {}; | ||
| if (options?.ttl !== void 0) taskParam.ttl = options.ttl; | ||
| const crossServer = options?.internal ?? deps.internalApp; | ||
| const callParams = { | ||
| name: toolName, | ||
| arguments: args ?? {}, | ||
| task: taskParam, | ||
| ...crossServer ? { server: deps.appName } : {} | ||
| }; | ||
| const raw = await deps.transport.request( | ||
| TOOLS_CALL_METHOD, | ||
| callParams | ||
| ); | ||
| const createResult = raw; | ||
| const initialTask = createResult?.task; | ||
| if (!initialTask || typeof initialTask !== "object" || typeof initialTask.taskId !== "string") { | ||
| throw new Error( | ||
| "callToolAsTask: receiver returned a response without `task` per CreateTaskResult (expected shape: `{ task: { taskId, status, ... } }`). Receiver may not honor the advertised tasks capability." | ||
| ); | ||
| } | ||
| const taskId = initialTask.taskId; | ||
| const localCallbacks = /* @__PURE__ */ new Map(); | ||
| const handle = { | ||
| task: initialTask, | ||
| async result() { | ||
| const params = { taskId }; | ||
| const rawResult = await deps.transport.request( | ||
| TASKS_RESULT_METHOD, | ||
| params | ||
| ); | ||
| const typed = rawResult; | ||
| return parseToolResult(typed); | ||
| }, | ||
| async refresh() { | ||
| const params = { taskId }; | ||
| const raw2 = await deps.transport.request( | ||
| TASKS_GET_METHOD, | ||
| params | ||
| ); | ||
| return projectTask(raw2); | ||
| }, | ||
| async cancel() { | ||
| const params = { taskId }; | ||
| const raw2 = await deps.transport.request( | ||
| TASKS_CANCEL_METHOD, | ||
| params | ||
| ); | ||
| return projectTask(raw2); | ||
| }, | ||
| onStatus(cb) { | ||
| const existing = localCallbacks.get(cb); | ||
| if (existing) return existing; | ||
| const wireUnsub = deps.router.subscribe(taskId, (update) => { | ||
| const merged = { | ||
| taskId: update.taskId, | ||
| status: update.status, | ||
| ttl: initialTask.ttl, | ||
| createdAt: initialTask.createdAt, | ||
| lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString(), | ||
| ...initialTask.pollInterval !== void 0 && { | ||
| pollInterval: initialTask.pollInterval | ||
| }, | ||
| ...update.statusMessage !== void 0 && { statusMessage: update.statusMessage } | ||
| }; | ||
| cb(merged); | ||
| }); | ||
| const unsub = () => { | ||
| localCallbacks.delete(cb); | ||
| wireUnsub(); | ||
| }; | ||
| localCallbacks.set(cb, unsub); | ||
| return unsub; | ||
| } | ||
| }; | ||
| return handle; | ||
| } | ||
| function projectTask(raw) { | ||
| return { | ||
| taskId: raw.taskId, | ||
| status: raw.status, | ||
| ttl: raw.ttl, | ||
| createdAt: raw.createdAt, | ||
| lastUpdatedAt: raw.lastUpdatedAt, | ||
| ...raw.pollInterval !== void 0 && { pollInterval: raw.pollInterval }, | ||
| ...raw.statusMessage !== void 0 && { statusMessage: raw.statusMessage } | ||
| }; | ||
| } | ||
| // src/core.ts | ||
| var READ_RESOURCE_METHOD2 = "resources/read"; | ||
| function createSynapse(options) { | ||
| const { name, version, internal = false, forwardKeys } = options; | ||
| const transport = new SynapseTransport(); | ||
| let hostInfo = null; | ||
| let currentHostContext = {}; | ||
| let hostTasksCapability = null; | ||
| let destroyed = false; | ||
| const taskStatusRouter = createTaskStatusRouter(transport); | ||
| let stateTimer = null; | ||
| let keyboard = null; | ||
| const appCapabilities = { | ||
| tasks: { | ||
| cancel: {}, | ||
| requests: { tools: { call: {} } } | ||
| } | ||
| }; | ||
| const initParams = { | ||
| protocolVersion: LATEST_PROTOCOL_VERSION, | ||
| appInfo: { name, version }, | ||
| appCapabilities | ||
| }; | ||
| const ready = transport.request(INITIALIZE_METHOD, initParams).then((result) => { | ||
| hostInfo = detectHost(result); | ||
| currentHostContext = result?.hostContext ?? {}; | ||
| const initResult = result; | ||
| const rawTasks = initResult?.hostCapabilities?.tasks; | ||
| hostTasksCapability = rawTasks && typeof rawTasks === "object" && !Array.isArray(rawTasks) ? rawTasks : void 0; | ||
| injectCssVariables2(extractTheme(currentHostContext).tokens); | ||
| for (const cb of hostContextCallbacks) cb(currentHostContext); | ||
| transport.send(INITIALIZED_METHOD, {}); | ||
| keyboard = new KeyboardForwarder(transport, forwardKeys); | ||
| }); | ||
| const unsubHostContext = transport.onMessage(HOST_CONTEXT_CHANGED_METHOD, (params) => { | ||
| currentHostContext = params ?? {}; | ||
| injectCssVariables2(extractTheme(currentHostContext).tokens); | ||
| for (const cb of hostContextCallbacks) cb(currentHostContext); | ||
| }); | ||
| const hostContextCallbacks = /* @__PURE__ */ new Set(); | ||
| const dataCallbacks = /* @__PURE__ */ new Set(); | ||
| const actionCallbacks = /* @__PURE__ */ new Set(); | ||
| const unsubData = transport.onMessage("synapse/data-changed", (params) => { | ||
| if (!params) return; | ||
| const event = { | ||
| source: "agent", | ||
| server: params.server ?? "", | ||
| tool: params.tool ?? "" | ||
| }; | ||
| for (const cb of dataCallbacks) cb(event); | ||
| }); | ||
| const unsubAction = transport.onMessage("synapse/action", (params) => { | ||
| if (!params || typeof params.type !== "string") return; | ||
| const action = { | ||
| type: params.type, | ||
| payload: params.payload ?? {}, | ||
| requiresConfirmation: params.requiresConfirmation === true, | ||
| label: typeof params.label === "string" ? params.label : void 0 | ||
| }; | ||
| for (const cb of actionCallbacks) cb(action); | ||
| }); | ||
| const isNB = () => hostInfo?.isNimbleBrain === true; | ||
| const synapse = { | ||
| get ready() { | ||
| return ready; | ||
| }, | ||
| get isNimbleBrainHost() { | ||
| return isNB(); | ||
| }, | ||
| get destroyed() { | ||
| return destroyed; | ||
| }, | ||
| async callTool(toolName, args) { | ||
| const params = { | ||
| name: toolName, | ||
| arguments: args ?? {} | ||
| }; | ||
| if (internal) { | ||
| params.server = name; | ||
| } | ||
| const raw = await transport.request("tools/call", params); | ||
| return parseToolResult(raw); | ||
| }, | ||
| async callToolAsTask(toolName, args, options2) { | ||
| return callToolAsTask( | ||
| { | ||
| transport, | ||
| router: taskStatusRouter, | ||
| getHostTasksCapability: () => hostTasksCapability, | ||
| appName: name, | ||
| internalApp: internal | ||
| }, | ||
| toolName, | ||
| args, | ||
| options2 | ||
| ); | ||
| }, | ||
| async readResource(uri) { | ||
| const params = { uri }; | ||
| const raw = await transport.request( | ||
| READ_RESOURCE_METHOD2, | ||
| params | ||
| ); | ||
| return raw; | ||
| }, | ||
| onDataChanged(callback) { | ||
| dataCallbacks.add(callback); | ||
| return () => { | ||
| dataCallbacks.delete(callback); | ||
| }; | ||
| }, | ||
| onAction(callback) { | ||
| actionCallbacks.add(callback); | ||
| return () => { | ||
| actionCallbacks.delete(callback); | ||
| }; | ||
| }, | ||
| getHostContext() { | ||
| return currentHostContext; | ||
| }, | ||
| onHostContextChanged(callback) { | ||
| hostContextCallbacks.add(callback); | ||
| return () => { | ||
| hostContextCallbacks.delete(callback); | ||
| }; | ||
| }, | ||
| getTheme() { | ||
| return extractTheme(currentHostContext); | ||
| }, | ||
| // Selector over `onHostContextChanged`: only fires when the *derived* | ||
| // theme actually changes, so theme subscribers don't see spurious | ||
| // updates when other host-context fields (e.g. workspace) change. | ||
| // | ||
| // Subscriber timing matters: | ||
| // - Subscribed BEFORE handshake: `prev = null` sentinel. The first | ||
| // fire (the handshake dispatch) always invokes the callback, | ||
| // even if the host's theme happens to derive to the default. | ||
| // Otherwise consumers using `onThemeChanged` as their init | ||
| // signal would silently miss it. | ||
| // - Subscribed AFTER handshake: `prev` is pre-seeded with the | ||
| // current derived theme, so a workspace-only `host-context-changed` | ||
| // notification correctly filters as a no-op. | ||
| onThemeChanged(callback) { | ||
| let prev = hostInfo !== null ? extractTheme(currentHostContext) : null; | ||
| const wrapped = (ctx) => { | ||
| const next = extractTheme(ctx); | ||
| if (prev !== null && themesEqual(prev, next)) return; | ||
| prev = next; | ||
| callback(next); | ||
| }; | ||
| hostContextCallbacks.add(wrapped); | ||
| return () => { | ||
| hostContextCallbacks.delete(wrapped); | ||
| }; | ||
| }, | ||
| action(action, params) { | ||
| if (!isNB()) return; | ||
| transport.send("synapse/action", { action, ...params }); | ||
| }, | ||
| chat(message, context) { | ||
| const textBlock = { | ||
| type: "text", | ||
| text: message, | ||
| ...isNB() && context && { _meta: { context } } | ||
| }; | ||
| const params = { | ||
| role: "user", | ||
| content: [textBlock] | ||
| }; | ||
| transport.send(MESSAGE_METHOD, params); | ||
| }, | ||
| setVisibleState(state, summary) { | ||
| if (stateTimer) clearTimeout(stateTimer); | ||
| stateTimer = setTimeout(() => { | ||
| const params = { | ||
| structuredContent: state, | ||
| ...summary !== void 0 && { | ||
| content: [{ type: "text", text: summary }] | ||
| } | ||
| }; | ||
| transport.send("ui/update-model-context", params); | ||
| stateTimer = null; | ||
| }, 250); | ||
| }, | ||
| downloadFile(filename, content, mimeType) { | ||
| const resolvedMime = mimeType || (content instanceof Blob ? content.type : "") || "application/octet-stream"; | ||
| const blob = content instanceof Blob ? content : new Blob([content], { type: resolvedMime }); | ||
| transport.send("synapse/download-file", { | ||
| data: blob, | ||
| filename, | ||
| mimeType: resolvedMime | ||
| }); | ||
| }, | ||
| openLink(url) { | ||
| const params = { url }; | ||
| transport.request(OPEN_LINK_METHOD, params).catch(() => { | ||
| window.open(url, "_blank", "noopener"); | ||
| }); | ||
| }, | ||
| async pickFile(options2) { | ||
| if (!isNB()) { | ||
| throw new Error("pickFile is not supported in this host"); | ||
| } | ||
| const result = await requestFile(transport, options2, false); | ||
| if (result === null) return null; | ||
| return Array.isArray(result) ? result[0] ?? null : result; | ||
| }, | ||
| async pickFiles(options2) { | ||
| if (!isNB()) { | ||
| throw new Error("pickFiles is not supported in this host"); | ||
| } | ||
| const result = await requestFile(transport, options2, true); | ||
| if (result === null) return []; | ||
| return Array.isArray(result) ? result : [result]; | ||
| }, | ||
| _onMessage(method, callback) { | ||
| return transport.onMessage(method, callback); | ||
| }, | ||
| _request(method, params) { | ||
| return transport.request(method, params); | ||
| }, | ||
| get _hostTasksCapability() { | ||
| return hostTasksCapability; | ||
| }, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| if (stateTimer) clearTimeout(stateTimer); | ||
| keyboard?.destroy(); | ||
| unsubHostContext(); | ||
| unsubData(); | ||
| unsubAction(); | ||
| taskStatusRouter.dispose(); | ||
| hostContextCallbacks.clear(); | ||
| dataCallbacks.clear(); | ||
| actionCallbacks.clear(); | ||
| transport.destroy(); | ||
| } | ||
| }; | ||
| return synapse; | ||
| } | ||
| async function requestFile(transport, options, multiple) { | ||
| const result = await transport.request("synapse/request-file", { | ||
| accept: options?.accept, | ||
| maxSize: options?.maxSize ?? 26214400, | ||
| multiple | ||
| }); | ||
| if (result == null) return null; | ||
| if (Array.isArray(result)) { | ||
| return result.map(validateFileResult); | ||
| } | ||
| return validateFileResult(result); | ||
| } | ||
| function validateFileResult(value) { | ||
| if (typeof value !== "object" || value === null || typeof value.id !== "string") { | ||
| throw new Error( | ||
| "synapse/request-file returned a result without a string `id`. The host appears to be on a version older than this SDK targets \u2014 @nimblebrain/synapse 0.8.0+ requires a host with POST /v1/resources (NimbleBrain \u2265 the version that ships PR #93)." | ||
| ); | ||
| } | ||
| return value; | ||
| } | ||
| function themesEqual(a, b) { | ||
| if (a.mode !== b.mode || a.primaryColor !== b.primaryColor) return false; | ||
| const aKeys = Object.keys(a.tokens); | ||
| const bKeys = Object.keys(b.tokens); | ||
| if (aKeys.length !== bKeys.length) return false; | ||
| for (const k of aKeys) { | ||
| if (a.tokens[k] !== b.tokens[k]) return false; | ||
| } | ||
| return true; | ||
| } | ||
| function injectCssVariables2(vars) { | ||
| if (!vars || typeof vars !== "object") return; | ||
| if (typeof document === "undefined") return; | ||
| for (const [k, v] of Object.entries(vars)) { | ||
| if (typeof k === "string" && typeof v === "string") { | ||
| document.documentElement.style.setProperty(k, v); | ||
| } | ||
| } | ||
| } | ||
| export { connect, createSynapse }; | ||
| //# sourceMappingURL=chunk-IQYOVYIR.js.map | ||
| //# sourceMappingURL=chunk-IQYOVYIR.js.map |
Sorry, the diff of this file is too big to display
| import { McpUiHostContext } from '@modelcontextprotocol/ext-apps'; | ||
| import { ReadResourceRequest, ReadResourceResult, Task } from '@modelcontextprotocol/sdk/types.js'; | ||
| /** | ||
| * Shape of the `tasks` capability advertised in `appCapabilities` on the | ||
| * iframe side (and mirrored back by the host in `hostCapabilities.tasks`). | ||
| * | ||
| * Matches the MCP 2025-11-25 tasks utility: empty objects (`{}`) are used | ||
| * as presence flags — NOT booleans — so future sub-fields can be added | ||
| * without wire-format breaks. | ||
| * | ||
| * Shape sourced from the MCP SDK's `ServerTasksCapabilitySchema` / | ||
| * `ClientCapabilities.tasks` contract. Defined locally as a plain | ||
| * interface because the SDK publishes the shape only as a Zod schema, | ||
| * not an exported TypeScript type — but the field names below are | ||
| * identical to the spec and will fail compilation against any SDK-typed | ||
| * consumer (e.g. `McpUiInitializeResult["hostCapabilities"]`) if they | ||
| * drift. | ||
| */ | ||
| interface TasksCapability { | ||
| /** Present (as `{}`) if listing tasks is supported. Deferred for MVP. */ | ||
| list?: Record<string, never>; | ||
| /** Present (as `{}`) if cancelling tasks is supported. */ | ||
| cancel?: Record<string, never>; | ||
| /** Which request types may be task-augmented. */ | ||
| requests?: { | ||
| tools?: { | ||
| /** Present (as `{}`) if `tools/call` can be task-augmented. */ | ||
| call?: Record<string, never>; | ||
| }; | ||
| }; | ||
| } | ||
| /** | ||
| * Options for task-augmenting a `tools/call` request per MCP 2025-11-25. | ||
| * | ||
| * The `task` object on `tools/call` params carries caller hints for task | ||
| * creation. The receiver MAY override (e.g. a server may enforce a lower | ||
| * TTL); clients read back the authoritative values from `CreateTaskResult.task`. | ||
| */ | ||
| interface CallToolAsTaskOptions { | ||
| /** | ||
| * Hint for how long (in milliseconds) the receiver should retain task | ||
| * results after a terminal status. Omit to let the receiver decide. | ||
| * Per spec, `null` means unlimited lifetime — represented here as the | ||
| * absence of the field (omit) since requestors rarely need to pin | ||
| * "unlimited" explicitly. | ||
| */ | ||
| ttl?: number; | ||
| /** | ||
| * Route the call through the internal-apps cross-server authz path | ||
| * (adds `params.server` set to this app's name). External apps MUST | ||
| * NOT pass this; spec doesn't touch it — it's a NimbleBrain-specific | ||
| * bridge convention mirroring `callTool`'s behavior. | ||
| */ | ||
| internal?: boolean; | ||
| } | ||
| /** | ||
| * Handle returned by `synapse.callToolAsTask`. Lifecycle mirrors the MCP | ||
| * 2025-11-25 tasks utility: the `tools/call` response is a | ||
| * `CreateTaskResult` (accessible via `task`), and the caller separately | ||
| * blocks for the terminal `CallToolResult` via `result()`. | ||
| * | ||
| * All operations route via the transport's message plumbing; no polling | ||
| * is performed here — `result()` is a blocking `tasks/result` RPC. If | ||
| * consumers want interstitial updates they can call `refresh()` or | ||
| * subscribe to `onStatus` (which is OPTIONAL per spec — hosts MAY or | ||
| * MAY NOT emit `notifications/tasks/status`). | ||
| */ | ||
| interface TaskHandle<TOutput = unknown> { | ||
| /** | ||
| * Initial task state from the `CreateTaskResult` returned by | ||
| * `tools/call`. Always populated before the handle is returned. | ||
| */ | ||
| readonly task: Task; | ||
| /** | ||
| * Send `tasks/result { taskId }` and resolve once the receiver returns | ||
| * the terminal payload. Per spec, the result shape is exactly what a | ||
| * non-task `tools/call` would return — parsed here via the shared | ||
| * `parseToolResult` so `_meta` (including | ||
| * `io.modelcontextprotocol/related-task`) propagates through. | ||
| */ | ||
| result(): Promise<ToolCallResult<TOutput>>; | ||
| /** | ||
| * Send `tasks/get { taskId }` and resolve with the current `Task`. | ||
| * Non-blocking — returns whatever status the receiver holds right now. | ||
| */ | ||
| refresh(): Promise<Task>; | ||
| /** | ||
| * Send `tasks/cancel { taskId }` and resolve with the final `Task` | ||
| * (expected `status: "cancelled"`). Cancelling an already-terminal | ||
| * task surfaces the receiver's `-32602` error. | ||
| */ | ||
| cancel(): Promise<Task>; | ||
| /** | ||
| * Subscribe to `notifications/tasks/status` events scoped to this | ||
| * handle's `taskId`. Returns an unsubscribe. Spec: status | ||
| * notifications are OPTIONAL; consumers MUST NOT depend on them for | ||
| * correctness. | ||
| */ | ||
| onStatus(cb: (task: Task) => void): () => void; | ||
| } | ||
| interface SynapseOptions { | ||
| /** App name — must match the bundle name registered with the host */ | ||
| name: string; | ||
| /** Semver version string */ | ||
| version: string; | ||
| /** | ||
| * Mark as internal NimbleBrain app. Enables cross-server tool calls. | ||
| * External apps MUST NOT set this. | ||
| */ | ||
| internal?: boolean; | ||
| /** Key combos to forward from iframe to host. Default: all Ctrl/Cmd combos + Escape. */ | ||
| forwardKeys?: KeyForwardConfig[]; | ||
| } | ||
| interface SynapseTheme { | ||
| mode: "light" | "dark"; | ||
| primaryColor: string; | ||
| tokens: Record<string, string>; | ||
| } | ||
| interface DataChangedEvent { | ||
| source: "agent"; | ||
| server: string; | ||
| tool: string; | ||
| } | ||
| /** | ||
| * Built-in action types that Synapse handles natively. | ||
| * | ||
| * - `navigate` — select/focus a resource in the UI (e.g., a board, document, record) | ||
| * - `notify` — display a transient message (toast/banner) | ||
| * - `refresh` — force a full data refresh (heavier than datachanged) | ||
| * - `confirm` — request user confirmation before the agent proceeds | ||
| * | ||
| * Apps may also receive custom string types for domain-specific actions. | ||
| */ | ||
| type BuiltinActionType = "navigate" | "notify" | "refresh" | "confirm"; | ||
| /** | ||
| * A typed, declarative action sent from the agent/server to the UI. | ||
| * | ||
| * Actions are deterministic side effects of tool execution — the tool decides | ||
| * what action to emit, not the LLM. The UI decides how to handle it. | ||
| * | ||
| * This mirrors Studio's ClientAction pattern, adapted for iframe postMessage. | ||
| */ | ||
| interface AgentAction<TPayload = Record<string, unknown>> { | ||
| /** Discriminator — a BuiltinActionType or custom string. */ | ||
| type: BuiltinActionType | (string & {}); | ||
| /** Typed payload — shape depends on `type`. */ | ||
| payload: TPayload; | ||
| /** If true, the UI should confirm with the user before executing. */ | ||
| requiresConfirmation?: boolean; | ||
| /** Human-readable label for confirmation dialogs or logs. */ | ||
| label?: string; | ||
| } | ||
| /** Payload for the built-in "navigate" action. */ | ||
| interface NavigatePayload { | ||
| /** Entity type (e.g., "board", "document", "task"). */ | ||
| entity: string; | ||
| /** Entity ID to select/focus. */ | ||
| id: string; | ||
| /** Optional sub-view or section within the entity. */ | ||
| view?: string; | ||
| } | ||
| /** Payload for the built-in "notify" action. */ | ||
| interface NotifyPayload { | ||
| message: string; | ||
| level?: "info" | "success" | "warning" | "error"; | ||
| } | ||
| interface ToolCallResult<T = unknown> { | ||
| data: T; | ||
| isError: boolean; | ||
| /** Raw MCP content blocks from the tool response. */ | ||
| content?: unknown[]; | ||
| /** | ||
| * `_meta` field from the underlying `CallToolResult`, passed through | ||
| * unchanged. Notably carries `io.modelcontextprotocol/related-task` | ||
| * (`{ taskId }`) on task-augmented results per MCP 2025-11-25. | ||
| * | ||
| * Key-preserving: any `_meta` entry the host/server attaches propagates | ||
| * without explicit support here. Consumers reading known keys should | ||
| * reference the canonical key names (e.g. `RELATED_TASK_META_KEY` from | ||
| * `@modelcontextprotocol/sdk/types.js`). | ||
| */ | ||
| _meta?: { | ||
| [key: string]: unknown; | ||
| }; | ||
| } | ||
| /** | ||
| * Result from a file picker request. Returned after the host has | ||
| * persisted the picked file to its workspace store; the bytes never | ||
| * cross the iframe boundary. | ||
| * | ||
| * Tools that need the bytes look the file up by `id` (e.g. by passing | ||
| * it to a tool that calls the host's file APIs server-side). Bytes | ||
| * inline in tool-call arguments was the prior shape and capped uploads | ||
| * at the JSON body limit; this `id`-shaped result removes that ceiling. | ||
| */ | ||
| interface FileResult { | ||
| /** Workspace file ID (`fl_` + 24 hex). Stable identifier for this | ||
| * file in the originating workspace. */ | ||
| id: string; | ||
| filename: string; | ||
| mimeType: string; | ||
| size: number; | ||
| } | ||
| /** Options for requesting a file from the user */ | ||
| interface RequestFileOptions { | ||
| /** File type filter (e.g., ".csv,.json", "image/*") */ | ||
| accept?: string; | ||
| /** Max file size in bytes. Default: 25 MB */ | ||
| maxSize?: number; | ||
| /** Allow multiple file selection. Default: false */ | ||
| multiple?: boolean; | ||
| } | ||
| interface Synapse { | ||
| readonly ready: Promise<void>; | ||
| readonly isNimbleBrainHost: boolean; | ||
| callTool<TInput = Record<string, unknown>, TOutput = unknown>(name: string, args?: TInput): Promise<ToolCallResult<TOutput>>; | ||
| /** | ||
| * Task-augmented variant of `callTool` per MCP 2025-11-25. Sends | ||
| * `tools/call` with a `task` param; the receiver returns a | ||
| * `CreateTaskResult` promptly and the actual `CallToolResult` lands | ||
| * via `tasks/result`. Returns a `TaskHandle` that exposes | ||
| * `result()`/`refresh()`/`cancel()`/`onStatus()`. | ||
| * | ||
| * Throws if the host did not advertise `tasks.requests.tools.call` in | ||
| * its init-response capabilities — requestors MUST NOT task-augment | ||
| * without matching receiver capability. | ||
| */ | ||
| callToolAsTask<TInput = Record<string, unknown>, TOutput = unknown>(name: string, args?: TInput, options?: CallToolAsTaskOptions): Promise<TaskHandle<TOutput>>; | ||
| /** | ||
| * Read an MCP resource from the originating server via the host bridge | ||
| * (ext-apps `resources/read`). | ||
| * | ||
| * Use this to resolve `resource_link` content blocks returned by tools, or | ||
| * to fetch any known resource URI exposed by the MCP server. The host | ||
| * proxies the request to the server and forwards the result unchanged. | ||
| * | ||
| * @param uri The resource URI (e.g. `"videos://bunny-1mb"`). | ||
| * @returns The server's `ReadResourceResult` — `contents` is an array of | ||
| * blocks, each with a `uri`, optional `mimeType`, and either `text` or | ||
| * `blob` (base64). | ||
| */ | ||
| readResource(uri: string): Promise<ReadResourceResult>; | ||
| onDataChanged(callback: (event: DataChangedEvent) => void): () => void; | ||
| /** | ||
| * Subscribe to agent actions — typed, declarative commands from the server. | ||
| * | ||
| * Actions are deterministic side effects of tool execution. The server/tool | ||
| * decides what action to emit; the UI decides how to handle it. | ||
| * | ||
| * The callback receives an AgentAction with a `type` discriminator and typed | ||
| * `payload`. Apps should handle known types and ignore unknown ones. | ||
| */ | ||
| onAction(callback: (action: AgentAction) => void): () => void; | ||
| /** | ||
| * Read the current ext-apps host context as last received from the host. | ||
| * | ||
| * Spec-standardized fields (`theme`, `styles`, `displayMode`, `toolInfo`) | ||
| * are typed; the open `[key: string]: unknown` allows hosts to publish | ||
| * extensions (e.g. NimbleBrain populates `workspace`). Apps reading | ||
| * host-specific fields should treat them as optional and tolerate | ||
| * missing values when running on other hosts. | ||
| * | ||
| * Returns the empty object before the `ui/initialize` handshake completes. | ||
| */ | ||
| getHostContext(): McpUiHostContext; | ||
| /** | ||
| * Subscribe to host-context updates. Fires once per | ||
| * `ui/notifications/host-context-changed` notification (which carries a | ||
| * full snapshot, not a delta) and once on handshake completion. | ||
| * | ||
| * `getTheme`/`onThemeChanged` are typed selectors over this same state — | ||
| * prefer them when only theming matters, since they filter no-op fires. | ||
| */ | ||
| onHostContextChanged(callback: (ctx: McpUiHostContext) => void): () => void; | ||
| getTheme(): SynapseTheme; | ||
| onThemeChanged(callback: (theme: SynapseTheme) => void): () => void; | ||
| /** NimbleBrain-only: trigger a host-side action. No-op in other hosts. */ | ||
| action(action: string, params?: Record<string, unknown>): void; | ||
| /** | ||
| * Send a user message into the agent conversation (ext-apps `ui/message`). | ||
| * | ||
| * @param context NimbleBrain-specific metadata, included as `_meta.context` | ||
| * on the content block. Ignored by non-NimbleBrain hosts. | ||
| */ | ||
| chat(message: string, context?: { | ||
| action?: string; | ||
| entity?: string; | ||
| }): void; | ||
| /** | ||
| * Push the app's current visible state to the agent (ext-apps `ui/update-model-context`). | ||
| * | ||
| * The `summary` string is what the LLM reads as a text content block. | ||
| * The `state` object is included as `structuredContent` for tools that need IDs/values. | ||
| * Debounced at 250ms. Each call overwrites the previous context. | ||
| */ | ||
| setVisibleState(state: Record<string, unknown>, summary?: string): void; | ||
| downloadFile(filename: string, content: string | Blob, mimeType?: string): void; | ||
| openLink(url: string): void; | ||
| /** | ||
| * Request a file from the user via the host's native file picker. | ||
| * NimbleBrain-only: throws in non-NimbleBrain hosts. | ||
| * Returns null if the user cancels. | ||
| */ | ||
| pickFile(options?: RequestFileOptions): Promise<FileResult | null>; | ||
| /** | ||
| * Pick multiple files from the user. | ||
| * NimbleBrain-only: throws in non-NimbleBrain hosts. | ||
| * Returns empty array if the user cancels. | ||
| */ | ||
| pickFiles(options?: RequestFileOptions): Promise<FileResult[]>; | ||
| /** @internal — used by createStore for synapse/state-loaded */ | ||
| _onMessage(method: string, callback: (params: Record<string, unknown> | undefined) => void): () => void; | ||
| /** @internal — used by createStore for synapse/persist-state */ | ||
| _request(method: string, params?: Record<string, unknown>): Promise<unknown>; | ||
| /** | ||
| * @internal — host's declared `tasks` capability from the `ui/initialize` | ||
| * response, or `undefined` if absent. Read by the task-augmented tool call | ||
| * path (future `callToolAsTask`) to decide whether task augmentation is | ||
| * negotiated. `null` before the handshake completes. | ||
| * | ||
| * Requestors MUST NOT task-augment a tool call unless this is defined and | ||
| * carries `requests.tools.call` per MCP 2025-11-25. | ||
| */ | ||
| readonly _hostTasksCapability: TasksCapability | undefined | null; | ||
| /** True after destroy() has been called. */ | ||
| readonly destroyed: boolean; | ||
| destroy(): void; | ||
| } | ||
| interface VisibleState { | ||
| state: Record<string, unknown>; | ||
| summary?: string; | ||
| } | ||
| interface StateAcknowledgement { | ||
| truncated: boolean; | ||
| } | ||
| type ActionReducer<TState, TPayload = unknown> = (state: TState, payload: TPayload) => TState; | ||
| interface StoreConfig<TState> { | ||
| initialState: TState; | ||
| actions: Record<string, ActionReducer<TState, any>>; | ||
| persist?: boolean; | ||
| visibleToAgent?: boolean; | ||
| summarize?: (state: TState) => string; | ||
| version?: number; | ||
| migrations?: Array<(oldState: any) => any>; | ||
| } | ||
| type StoreDispatch<TActions extends Record<string, ActionReducer<any, any>>> = { | ||
| [K in keyof TActions]: Parameters<TActions[K]>[1] extends undefined ? () => void : (payload: Parameters<TActions[K]>[1]) => void; | ||
| }; | ||
| interface Store<TState, TActions extends Record<string, ActionReducer<TState, any>> = Record<string, ActionReducer<TState, any>>> { | ||
| getState(): TState; | ||
| subscribe(callback: (state: TState) => void): () => void; | ||
| dispatch: StoreDispatch<TActions>; | ||
| hydrate(state: TState): void; | ||
| destroy(): void; | ||
| } | ||
| interface KeyForwardConfig { | ||
| key: string; | ||
| ctrl?: boolean; | ||
| meta?: boolean; | ||
| shift?: boolean; | ||
| alt?: boolean; | ||
| } | ||
| interface ToolDefinition { | ||
| name: string; | ||
| description?: string; | ||
| inputSchema: Record<string, unknown>; | ||
| outputSchema?: Record<string, unknown>; | ||
| } | ||
| interface HostInfo { | ||
| isNimbleBrain: boolean; | ||
| serverName: string; | ||
| protocolVersion: string; | ||
| } | ||
| interface ConnectOptions { | ||
| name: string; | ||
| version: string; | ||
| autoResize?: boolean; | ||
| /** Pre-register event handlers before the handshake completes. | ||
| * These are wired before `initialized` is sent, so no messages are lost. */ | ||
| on?: Record<string, (data: any) => void>; | ||
| } | ||
| interface Theme { | ||
| mode: "light" | "dark"; | ||
| tokens: Record<string, string>; | ||
| } | ||
| interface Dimensions { | ||
| width?: number; | ||
| height?: number; | ||
| maxWidth?: number; | ||
| maxHeight?: number; | ||
| } | ||
| interface ToolResultData { | ||
| content: unknown; | ||
| structuredContent: unknown; | ||
| raw: Record<string, unknown>; | ||
| } | ||
| /** Known short event names for App.on() */ | ||
| type AppEventName = "tool-result" | "tool-input" | "tool-input-partial" | "tool-cancelled" | "theme-changed" | "teardown"; | ||
| interface App { | ||
| readonly theme: Theme; | ||
| readonly hostInfo: { | ||
| name: string; | ||
| version: string; | ||
| }; | ||
| readonly toolInfo: { | ||
| tool: Record<string, unknown>; | ||
| } | null; | ||
| readonly containerDimensions: Dimensions | null; | ||
| on(event: "tool-input", handler: (args: Record<string, unknown>) => void): () => void; | ||
| on(event: "tool-result", handler: (data: ToolResultData) => void): () => void; | ||
| on(event: "theme-changed", handler: (theme: Theme) => void): () => void; | ||
| on(event: "teardown", handler: () => void): () => void; | ||
| on(event: string, handler: (params: unknown) => void): () => void; | ||
| resize(width?: number, height?: number): void; | ||
| openLink(url: string): void; | ||
| updateModelContext(state: Record<string, unknown>, summary?: string): void; | ||
| callTool(name: string, args?: Record<string, unknown>): Promise<ToolCallResult>; | ||
| /** | ||
| * Read an MCP resource from the originating server via the host bridge | ||
| * (ext-apps `resources/read`). Named to mirror the ext-apps spec's | ||
| * `App.readServerResource`. | ||
| */ | ||
| readServerResource(params: ReadResourceRequest["params"]): Promise<ReadResourceResult>; | ||
| sendMessage(text: string, context?: { | ||
| action?: string; | ||
| entity?: string; | ||
| }): void; | ||
| destroy(): void; | ||
| } | ||
| export type { App as A, BuiltinActionType as B, ConnectOptions as C, DataChangedEvent as D, FileResult as F, HostInfo as H, KeyForwardConfig as K, NavigatePayload as N, RequestFileOptions as R, SynapseOptions as S, ToolDefinition as T, VisibleState as V, Synapse as a, ActionReducer as b, StoreConfig as c, Store as d, AgentAction as e, AppEventName as f, CallToolAsTaskOptions as g, Dimensions as h, NotifyPayload as i, StateAcknowledgement as j, StoreDispatch as k, SynapseTheme as l, TaskHandle as m, TasksCapability as n, Theme as o, ToolCallResult as p, ToolResultData as q }; |
| import { McpUiHostContext } from '@modelcontextprotocol/ext-apps'; | ||
| import { ReadResourceRequest, ReadResourceResult, Task } from '@modelcontextprotocol/sdk/types.js'; | ||
| /** | ||
| * Shape of the `tasks` capability advertised in `appCapabilities` on the | ||
| * iframe side (and mirrored back by the host in `hostCapabilities.tasks`). | ||
| * | ||
| * Matches the MCP 2025-11-25 tasks utility: empty objects (`{}`) are used | ||
| * as presence flags — NOT booleans — so future sub-fields can be added | ||
| * without wire-format breaks. | ||
| * | ||
| * Shape sourced from the MCP SDK's `ServerTasksCapabilitySchema` / | ||
| * `ClientCapabilities.tasks` contract. Defined locally as a plain | ||
| * interface because the SDK publishes the shape only as a Zod schema, | ||
| * not an exported TypeScript type — but the field names below are | ||
| * identical to the spec and will fail compilation against any SDK-typed | ||
| * consumer (e.g. `McpUiInitializeResult["hostCapabilities"]`) if they | ||
| * drift. | ||
| */ | ||
| interface TasksCapability { | ||
| /** Present (as `{}`) if listing tasks is supported. Deferred for MVP. */ | ||
| list?: Record<string, never>; | ||
| /** Present (as `{}`) if cancelling tasks is supported. */ | ||
| cancel?: Record<string, never>; | ||
| /** Which request types may be task-augmented. */ | ||
| requests?: { | ||
| tools?: { | ||
| /** Present (as `{}`) if `tools/call` can be task-augmented. */ | ||
| call?: Record<string, never>; | ||
| }; | ||
| }; | ||
| } | ||
| /** | ||
| * Options for task-augmenting a `tools/call` request per MCP 2025-11-25. | ||
| * | ||
| * The `task` object on `tools/call` params carries caller hints for task | ||
| * creation. The receiver MAY override (e.g. a server may enforce a lower | ||
| * TTL); clients read back the authoritative values from `CreateTaskResult.task`. | ||
| */ | ||
| interface CallToolAsTaskOptions { | ||
| /** | ||
| * Hint for how long (in milliseconds) the receiver should retain task | ||
| * results after a terminal status. Omit to let the receiver decide. | ||
| * Per spec, `null` means unlimited lifetime — represented here as the | ||
| * absence of the field (omit) since requestors rarely need to pin | ||
| * "unlimited" explicitly. | ||
| */ | ||
| ttl?: number; | ||
| /** | ||
| * Route the call through the internal-apps cross-server authz path | ||
| * (adds `params.server` set to this app's name). External apps MUST | ||
| * NOT pass this; spec doesn't touch it — it's a NimbleBrain-specific | ||
| * bridge convention mirroring `callTool`'s behavior. | ||
| */ | ||
| internal?: boolean; | ||
| } | ||
| /** | ||
| * Handle returned by `synapse.callToolAsTask`. Lifecycle mirrors the MCP | ||
| * 2025-11-25 tasks utility: the `tools/call` response is a | ||
| * `CreateTaskResult` (accessible via `task`), and the caller separately | ||
| * blocks for the terminal `CallToolResult` via `result()`. | ||
| * | ||
| * All operations route via the transport's message plumbing; no polling | ||
| * is performed here — `result()` is a blocking `tasks/result` RPC. If | ||
| * consumers want interstitial updates they can call `refresh()` or | ||
| * subscribe to `onStatus` (which is OPTIONAL per spec — hosts MAY or | ||
| * MAY NOT emit `notifications/tasks/status`). | ||
| */ | ||
| interface TaskHandle<TOutput = unknown> { | ||
| /** | ||
| * Initial task state from the `CreateTaskResult` returned by | ||
| * `tools/call`. Always populated before the handle is returned. | ||
| */ | ||
| readonly task: Task; | ||
| /** | ||
| * Send `tasks/result { taskId }` and resolve once the receiver returns | ||
| * the terminal payload. Per spec, the result shape is exactly what a | ||
| * non-task `tools/call` would return — parsed here via the shared | ||
| * `parseToolResult` so `_meta` (including | ||
| * `io.modelcontextprotocol/related-task`) propagates through. | ||
| */ | ||
| result(): Promise<ToolCallResult<TOutput>>; | ||
| /** | ||
| * Send `tasks/get { taskId }` and resolve with the current `Task`. | ||
| * Non-blocking — returns whatever status the receiver holds right now. | ||
| */ | ||
| refresh(): Promise<Task>; | ||
| /** | ||
| * Send `tasks/cancel { taskId }` and resolve with the final `Task` | ||
| * (expected `status: "cancelled"`). Cancelling an already-terminal | ||
| * task surfaces the receiver's `-32602` error. | ||
| */ | ||
| cancel(): Promise<Task>; | ||
| /** | ||
| * Subscribe to `notifications/tasks/status` events scoped to this | ||
| * handle's `taskId`. Returns an unsubscribe. Spec: status | ||
| * notifications are OPTIONAL; consumers MUST NOT depend on them for | ||
| * correctness. | ||
| */ | ||
| onStatus(cb: (task: Task) => void): () => void; | ||
| } | ||
| interface SynapseOptions { | ||
| /** App name — must match the bundle name registered with the host */ | ||
| name: string; | ||
| /** Semver version string */ | ||
| version: string; | ||
| /** | ||
| * Mark as internal NimbleBrain app. Enables cross-server tool calls. | ||
| * External apps MUST NOT set this. | ||
| */ | ||
| internal?: boolean; | ||
| /** Key combos to forward from iframe to host. Default: all Ctrl/Cmd combos + Escape. */ | ||
| forwardKeys?: KeyForwardConfig[]; | ||
| } | ||
| interface SynapseTheme { | ||
| mode: "light" | "dark"; | ||
| primaryColor: string; | ||
| tokens: Record<string, string>; | ||
| } | ||
| interface DataChangedEvent { | ||
| source: "agent"; | ||
| server: string; | ||
| tool: string; | ||
| } | ||
| /** | ||
| * Built-in action types that Synapse handles natively. | ||
| * | ||
| * - `navigate` — select/focus a resource in the UI (e.g., a board, document, record) | ||
| * - `notify` — display a transient message (toast/banner) | ||
| * - `refresh` — force a full data refresh (heavier than datachanged) | ||
| * - `confirm` — request user confirmation before the agent proceeds | ||
| * | ||
| * Apps may also receive custom string types for domain-specific actions. | ||
| */ | ||
| type BuiltinActionType = "navigate" | "notify" | "refresh" | "confirm"; | ||
| /** | ||
| * A typed, declarative action sent from the agent/server to the UI. | ||
| * | ||
| * Actions are deterministic side effects of tool execution — the tool decides | ||
| * what action to emit, not the LLM. The UI decides how to handle it. | ||
| * | ||
| * This mirrors Studio's ClientAction pattern, adapted for iframe postMessage. | ||
| */ | ||
| interface AgentAction<TPayload = Record<string, unknown>> { | ||
| /** Discriminator — a BuiltinActionType or custom string. */ | ||
| type: BuiltinActionType | (string & {}); | ||
| /** Typed payload — shape depends on `type`. */ | ||
| payload: TPayload; | ||
| /** If true, the UI should confirm with the user before executing. */ | ||
| requiresConfirmation?: boolean; | ||
| /** Human-readable label for confirmation dialogs or logs. */ | ||
| label?: string; | ||
| } | ||
| /** Payload for the built-in "navigate" action. */ | ||
| interface NavigatePayload { | ||
| /** Entity type (e.g., "board", "document", "task"). */ | ||
| entity: string; | ||
| /** Entity ID to select/focus. */ | ||
| id: string; | ||
| /** Optional sub-view or section within the entity. */ | ||
| view?: string; | ||
| } | ||
| /** Payload for the built-in "notify" action. */ | ||
| interface NotifyPayload { | ||
| message: string; | ||
| level?: "info" | "success" | "warning" | "error"; | ||
| } | ||
| interface ToolCallResult<T = unknown> { | ||
| data: T; | ||
| isError: boolean; | ||
| /** Raw MCP content blocks from the tool response. */ | ||
| content?: unknown[]; | ||
| /** | ||
| * `_meta` field from the underlying `CallToolResult`, passed through | ||
| * unchanged. Notably carries `io.modelcontextprotocol/related-task` | ||
| * (`{ taskId }`) on task-augmented results per MCP 2025-11-25. | ||
| * | ||
| * Key-preserving: any `_meta` entry the host/server attaches propagates | ||
| * without explicit support here. Consumers reading known keys should | ||
| * reference the canonical key names (e.g. `RELATED_TASK_META_KEY` from | ||
| * `@modelcontextprotocol/sdk/types.js`). | ||
| */ | ||
| _meta?: { | ||
| [key: string]: unknown; | ||
| }; | ||
| } | ||
| /** | ||
| * Result from a file picker request. Returned after the host has | ||
| * persisted the picked file to its workspace store; the bytes never | ||
| * cross the iframe boundary. | ||
| * | ||
| * Tools that need the bytes look the file up by `id` (e.g. by passing | ||
| * it to a tool that calls the host's file APIs server-side). Bytes | ||
| * inline in tool-call arguments was the prior shape and capped uploads | ||
| * at the JSON body limit; this `id`-shaped result removes that ceiling. | ||
| */ | ||
| interface FileResult { | ||
| /** Workspace file ID (`fl_` + 24 hex). Stable identifier for this | ||
| * file in the originating workspace. */ | ||
| id: string; | ||
| filename: string; | ||
| mimeType: string; | ||
| size: number; | ||
| } | ||
| /** Options for requesting a file from the user */ | ||
| interface RequestFileOptions { | ||
| /** File type filter (e.g., ".csv,.json", "image/*") */ | ||
| accept?: string; | ||
| /** Max file size in bytes. Default: 25 MB */ | ||
| maxSize?: number; | ||
| /** Allow multiple file selection. Default: false */ | ||
| multiple?: boolean; | ||
| } | ||
| interface Synapse { | ||
| readonly ready: Promise<void>; | ||
| readonly isNimbleBrainHost: boolean; | ||
| callTool<TInput = Record<string, unknown>, TOutput = unknown>(name: string, args?: TInput): Promise<ToolCallResult<TOutput>>; | ||
| /** | ||
| * Task-augmented variant of `callTool` per MCP 2025-11-25. Sends | ||
| * `tools/call` with a `task` param; the receiver returns a | ||
| * `CreateTaskResult` promptly and the actual `CallToolResult` lands | ||
| * via `tasks/result`. Returns a `TaskHandle` that exposes | ||
| * `result()`/`refresh()`/`cancel()`/`onStatus()`. | ||
| * | ||
| * Throws if the host did not advertise `tasks.requests.tools.call` in | ||
| * its init-response capabilities — requestors MUST NOT task-augment | ||
| * without matching receiver capability. | ||
| */ | ||
| callToolAsTask<TInput = Record<string, unknown>, TOutput = unknown>(name: string, args?: TInput, options?: CallToolAsTaskOptions): Promise<TaskHandle<TOutput>>; | ||
| /** | ||
| * Read an MCP resource from the originating server via the host bridge | ||
| * (ext-apps `resources/read`). | ||
| * | ||
| * Use this to resolve `resource_link` content blocks returned by tools, or | ||
| * to fetch any known resource URI exposed by the MCP server. The host | ||
| * proxies the request to the server and forwards the result unchanged. | ||
| * | ||
| * @param uri The resource URI (e.g. `"videos://bunny-1mb"`). | ||
| * @returns The server's `ReadResourceResult` — `contents` is an array of | ||
| * blocks, each with a `uri`, optional `mimeType`, and either `text` or | ||
| * `blob` (base64). | ||
| */ | ||
| readResource(uri: string): Promise<ReadResourceResult>; | ||
| onDataChanged(callback: (event: DataChangedEvent) => void): () => void; | ||
| /** | ||
| * Subscribe to agent actions — typed, declarative commands from the server. | ||
| * | ||
| * Actions are deterministic side effects of tool execution. The server/tool | ||
| * decides what action to emit; the UI decides how to handle it. | ||
| * | ||
| * The callback receives an AgentAction with a `type` discriminator and typed | ||
| * `payload`. Apps should handle known types and ignore unknown ones. | ||
| */ | ||
| onAction(callback: (action: AgentAction) => void): () => void; | ||
| /** | ||
| * Read the current ext-apps host context as last received from the host. | ||
| * | ||
| * Spec-standardized fields (`theme`, `styles`, `displayMode`, `toolInfo`) | ||
| * are typed; the open `[key: string]: unknown` allows hosts to publish | ||
| * extensions (e.g. NimbleBrain populates `workspace`). Apps reading | ||
| * host-specific fields should treat them as optional and tolerate | ||
| * missing values when running on other hosts. | ||
| * | ||
| * Returns the empty object before the `ui/initialize` handshake completes. | ||
| */ | ||
| getHostContext(): McpUiHostContext; | ||
| /** | ||
| * Subscribe to host-context updates. Fires once per | ||
| * `ui/notifications/host-context-changed` notification (which carries a | ||
| * full snapshot, not a delta) and once on handshake completion. | ||
| * | ||
| * `getTheme`/`onThemeChanged` are typed selectors over this same state — | ||
| * prefer them when only theming matters, since they filter no-op fires. | ||
| */ | ||
| onHostContextChanged(callback: (ctx: McpUiHostContext) => void): () => void; | ||
| getTheme(): SynapseTheme; | ||
| onThemeChanged(callback: (theme: SynapseTheme) => void): () => void; | ||
| /** NimbleBrain-only: trigger a host-side action. No-op in other hosts. */ | ||
| action(action: string, params?: Record<string, unknown>): void; | ||
| /** | ||
| * Send a user message into the agent conversation (ext-apps `ui/message`). | ||
| * | ||
| * @param context NimbleBrain-specific metadata, included as `_meta.context` | ||
| * on the content block. Ignored by non-NimbleBrain hosts. | ||
| */ | ||
| chat(message: string, context?: { | ||
| action?: string; | ||
| entity?: string; | ||
| }): void; | ||
| /** | ||
| * Push the app's current visible state to the agent (ext-apps `ui/update-model-context`). | ||
| * | ||
| * The `summary` string is what the LLM reads as a text content block. | ||
| * The `state` object is included as `structuredContent` for tools that need IDs/values. | ||
| * Debounced at 250ms. Each call overwrites the previous context. | ||
| */ | ||
| setVisibleState(state: Record<string, unknown>, summary?: string): void; | ||
| downloadFile(filename: string, content: string | Blob, mimeType?: string): void; | ||
| openLink(url: string): void; | ||
| /** | ||
| * Request a file from the user via the host's native file picker. | ||
| * NimbleBrain-only: throws in non-NimbleBrain hosts. | ||
| * Returns null if the user cancels. | ||
| */ | ||
| pickFile(options?: RequestFileOptions): Promise<FileResult | null>; | ||
| /** | ||
| * Pick multiple files from the user. | ||
| * NimbleBrain-only: throws in non-NimbleBrain hosts. | ||
| * Returns empty array if the user cancels. | ||
| */ | ||
| pickFiles(options?: RequestFileOptions): Promise<FileResult[]>; | ||
| /** @internal — used by createStore for synapse/state-loaded */ | ||
| _onMessage(method: string, callback: (params: Record<string, unknown> | undefined) => void): () => void; | ||
| /** @internal — used by createStore for synapse/persist-state */ | ||
| _request(method: string, params?: Record<string, unknown>): Promise<unknown>; | ||
| /** | ||
| * @internal — host's declared `tasks` capability from the `ui/initialize` | ||
| * response, or `undefined` if absent. Read by the task-augmented tool call | ||
| * path (future `callToolAsTask`) to decide whether task augmentation is | ||
| * negotiated. `null` before the handshake completes. | ||
| * | ||
| * Requestors MUST NOT task-augment a tool call unless this is defined and | ||
| * carries `requests.tools.call` per MCP 2025-11-25. | ||
| */ | ||
| readonly _hostTasksCapability: TasksCapability | undefined | null; | ||
| /** True after destroy() has been called. */ | ||
| readonly destroyed: boolean; | ||
| destroy(): void; | ||
| } | ||
| interface VisibleState { | ||
| state: Record<string, unknown>; | ||
| summary?: string; | ||
| } | ||
| interface StateAcknowledgement { | ||
| truncated: boolean; | ||
| } | ||
| type ActionReducer<TState, TPayload = unknown> = (state: TState, payload: TPayload) => TState; | ||
| interface StoreConfig<TState> { | ||
| initialState: TState; | ||
| actions: Record<string, ActionReducer<TState, any>>; | ||
| persist?: boolean; | ||
| visibleToAgent?: boolean; | ||
| summarize?: (state: TState) => string; | ||
| version?: number; | ||
| migrations?: Array<(oldState: any) => any>; | ||
| } | ||
| type StoreDispatch<TActions extends Record<string, ActionReducer<any, any>>> = { | ||
| [K in keyof TActions]: Parameters<TActions[K]>[1] extends undefined ? () => void : (payload: Parameters<TActions[K]>[1]) => void; | ||
| }; | ||
| interface Store<TState, TActions extends Record<string, ActionReducer<TState, any>> = Record<string, ActionReducer<TState, any>>> { | ||
| getState(): TState; | ||
| subscribe(callback: (state: TState) => void): () => void; | ||
| dispatch: StoreDispatch<TActions>; | ||
| hydrate(state: TState): void; | ||
| destroy(): void; | ||
| } | ||
| interface KeyForwardConfig { | ||
| key: string; | ||
| ctrl?: boolean; | ||
| meta?: boolean; | ||
| shift?: boolean; | ||
| alt?: boolean; | ||
| } | ||
| interface ToolDefinition { | ||
| name: string; | ||
| description?: string; | ||
| inputSchema: Record<string, unknown>; | ||
| outputSchema?: Record<string, unknown>; | ||
| } | ||
| interface HostInfo { | ||
| isNimbleBrain: boolean; | ||
| serverName: string; | ||
| protocolVersion: string; | ||
| } | ||
| interface ConnectOptions { | ||
| name: string; | ||
| version: string; | ||
| autoResize?: boolean; | ||
| /** Pre-register event handlers before the handshake completes. | ||
| * These are wired before `initialized` is sent, so no messages are lost. */ | ||
| on?: Record<string, (data: any) => void>; | ||
| } | ||
| interface Theme { | ||
| mode: "light" | "dark"; | ||
| tokens: Record<string, string>; | ||
| } | ||
| interface Dimensions { | ||
| width?: number; | ||
| height?: number; | ||
| maxWidth?: number; | ||
| maxHeight?: number; | ||
| } | ||
| interface ToolResultData { | ||
| content: unknown; | ||
| structuredContent: unknown; | ||
| raw: Record<string, unknown>; | ||
| } | ||
| /** Known short event names for App.on() */ | ||
| type AppEventName = "tool-result" | "tool-input" | "tool-input-partial" | "tool-cancelled" | "theme-changed" | "teardown"; | ||
| interface App { | ||
| readonly theme: Theme; | ||
| readonly hostInfo: { | ||
| name: string; | ||
| version: string; | ||
| }; | ||
| readonly toolInfo: { | ||
| tool: Record<string, unknown>; | ||
| } | null; | ||
| readonly containerDimensions: Dimensions | null; | ||
| on(event: "tool-input", handler: (args: Record<string, unknown>) => void): () => void; | ||
| on(event: "tool-result", handler: (data: ToolResultData) => void): () => void; | ||
| on(event: "theme-changed", handler: (theme: Theme) => void): () => void; | ||
| on(event: "teardown", handler: () => void): () => void; | ||
| on(event: string, handler: (params: unknown) => void): () => void; | ||
| resize(width?: number, height?: number): void; | ||
| openLink(url: string): void; | ||
| updateModelContext(state: Record<string, unknown>, summary?: string): void; | ||
| callTool(name: string, args?: Record<string, unknown>): Promise<ToolCallResult>; | ||
| /** | ||
| * Read an MCP resource from the originating server via the host bridge | ||
| * (ext-apps `resources/read`). Named to mirror the ext-apps spec's | ||
| * `App.readServerResource`. | ||
| */ | ||
| readServerResource(params: ReadResourceRequest["params"]): Promise<ReadResourceResult>; | ||
| sendMessage(text: string, context?: { | ||
| action?: string; | ||
| entity?: string; | ||
| }): void; | ||
| destroy(): void; | ||
| } | ||
| export type { App as A, BuiltinActionType as B, ConnectOptions as C, DataChangedEvent as D, FileResult as F, HostInfo as H, KeyForwardConfig as K, NavigatePayload as N, RequestFileOptions as R, SynapseOptions as S, ToolDefinition as T, VisibleState as V, Synapse as a, ActionReducer as b, StoreConfig as c, Store as d, AgentAction as e, AppEventName as f, CallToolAsTaskOptions as g, Dimensions as h, NotifyPayload as i, StateAcknowledgement as j, StoreDispatch as k, SynapseTheme as l, TaskHandle as m, TasksCapability as n, Theme as o, ToolCallResult as p, ToolResultData as q }; |
@@ -1,2 +0,2 @@ | ||
| import { T as ToolDefinition } from '../types-Beo6hjvb.cjs'; | ||
| import { T as ToolDefinition } from '../types-C9utGXv_.cjs'; | ||
| import '@modelcontextprotocol/ext-apps'; | ||
@@ -3,0 +3,0 @@ import '@modelcontextprotocol/sdk/types.js'; |
@@ -1,2 +0,2 @@ | ||
| import { T as ToolDefinition } from '../types-Beo6hjvb.js'; | ||
| import { T as ToolDefinition } from '../types-C9utGXv_.js'; | ||
| import '@modelcontextprotocol/ext-apps'; | ||
@@ -3,0 +3,0 @@ import '@modelcontextprotocol/sdk/types.js'; |
+3
-3
| 'use strict'; | ||
| var chunkWYWKN3YG_cjs = require('./chunk-WYWKN3YG.cjs'); | ||
| var chunk42JAT6TK_cjs = require('./chunk-42JAT6TK.cjs'); | ||
@@ -83,7 +83,7 @@ // src/store.ts | ||
| enumerable: true, | ||
| get: function () { return chunkWYWKN3YG_cjs.connect; } | ||
| get: function () { return chunk42JAT6TK_cjs.connect; } | ||
| }); | ||
| Object.defineProperty(exports, "createSynapse", { | ||
| enumerable: true, | ||
| get: function () { return chunkWYWKN3YG_cjs.createSynapse; } | ||
| get: function () { return chunk42JAT6TK_cjs.createSynapse; } | ||
| }); | ||
@@ -90,0 +90,0 @@ exports.createStore = createStore; |
+2
-2
| export { CreateTaskResult, ReadResourceRequest, ReadResourceResult, Task, TaskStatus } from '@modelcontextprotocol/sdk/types.js'; | ||
| import { C as ConnectOptions, A as App, S as SynapseOptions, a as Synapse, b as ActionReducer, c as StoreConfig, d as Store } from './types-Beo6hjvb.cjs'; | ||
| export { e as AgentAction, f as AppEventName, B as BuiltinActionType, g as CallToolAsTaskOptions, D as DataChangedEvent, h as Dimensions, F as FileResult, H as HostInfo, K as KeyForwardConfig, N as NavigatePayload, i as NotifyPayload, R as RequestFileOptions, j as StateAcknowledgement, k as StoreDispatch, l as SynapseTheme, m as TaskHandle, n as TasksCapability, o as Theme, p as ToolCallResult, T as ToolDefinition, q as ToolResultData, V as VisibleState } from './types-Beo6hjvb.cjs'; | ||
| import { C as ConnectOptions, A as App, S as SynapseOptions, a as Synapse, b as ActionReducer, c as StoreConfig, d as Store } from './types-C9utGXv_.cjs'; | ||
| export { e as AgentAction, f as AppEventName, B as BuiltinActionType, g as CallToolAsTaskOptions, D as DataChangedEvent, h as Dimensions, F as FileResult, H as HostInfo, K as KeyForwardConfig, N as NavigatePayload, i as NotifyPayload, R as RequestFileOptions, j as StateAcknowledgement, k as StoreDispatch, l as SynapseTheme, m as TaskHandle, n as TasksCapability, o as Theme, p as ToolCallResult, T as ToolDefinition, q as ToolResultData, V as VisibleState } from './types-C9utGXv_.cjs'; | ||
| import '@modelcontextprotocol/ext-apps'; | ||
@@ -5,0 +5,0 @@ |
+2
-2
| export { CreateTaskResult, ReadResourceRequest, ReadResourceResult, Task, TaskStatus } from '@modelcontextprotocol/sdk/types.js'; | ||
| import { C as ConnectOptions, A as App, S as SynapseOptions, a as Synapse, b as ActionReducer, c as StoreConfig, d as Store } from './types-Beo6hjvb.js'; | ||
| export { e as AgentAction, f as AppEventName, B as BuiltinActionType, g as CallToolAsTaskOptions, D as DataChangedEvent, h as Dimensions, F as FileResult, H as HostInfo, K as KeyForwardConfig, N as NavigatePayload, i as NotifyPayload, R as RequestFileOptions, j as StateAcknowledgement, k as StoreDispatch, l as SynapseTheme, m as TaskHandle, n as TasksCapability, o as Theme, p as ToolCallResult, T as ToolDefinition, q as ToolResultData, V as VisibleState } from './types-Beo6hjvb.js'; | ||
| import { C as ConnectOptions, A as App, S as SynapseOptions, a as Synapse, b as ActionReducer, c as StoreConfig, d as Store } from './types-C9utGXv_.js'; | ||
| export { e as AgentAction, f as AppEventName, B as BuiltinActionType, g as CallToolAsTaskOptions, D as DataChangedEvent, h as Dimensions, F as FileResult, H as HostInfo, K as KeyForwardConfig, N as NavigatePayload, i as NotifyPayload, R as RequestFileOptions, j as StateAcknowledgement, k as StoreDispatch, l as SynapseTheme, m as TaskHandle, n as TasksCapability, o as Theme, p as ToolCallResult, T as ToolDefinition, q as ToolResultData, V as VisibleState } from './types-C9utGXv_.js'; | ||
| import '@modelcontextprotocol/ext-apps'; | ||
@@ -5,0 +5,0 @@ |
+1
-1
@@ -1,2 +0,2 @@ | ||
| export { connect, createSynapse } from './chunk-2HKN2A2J.js'; | ||
| export { connect, createSynapse } from './chunk-IQYOVYIR.js'; | ||
@@ -3,0 +3,0 @@ // src/store.ts |
| 'use strict'; | ||
| var chunkWYWKN3YG_cjs = require('../chunk-WYWKN3YG.cjs'); | ||
| var chunk42JAT6TK_cjs = require('../chunk-42JAT6TK.cjs'); | ||
| var react = require('react'); | ||
@@ -14,3 +14,3 @@ var jsxRuntime = require('react/jsx-runtime'); | ||
| connectingRef.current = true; | ||
| chunkWYWKN3YG_cjs.connect({ name, version, autoResize }).then((a) => { | ||
| chunk42JAT6TK_cjs.connect({ name, version, autoResize }).then((a) => { | ||
| setApp(a); | ||
@@ -75,3 +75,3 @@ }); | ||
| if (ref.current === null || ref.current.destroyed) { | ||
| ref.current = chunkWYWKN3YG_cjs.createSynapse(options); | ||
| ref.current = chunk42JAT6TK_cjs.createSynapse(options); | ||
| } | ||
@@ -78,0 +78,0 @@ return /* @__PURE__ */ jsxRuntime.jsxs(SynapseContext.Provider, { value: ref.current, children: [ |
| import * as react_jsx_runtime from 'react/jsx-runtime'; | ||
| import { ReactNode } from 'react'; | ||
| import { C as ConnectOptions, A as App, o as Theme, q as ToolResultData, S as SynapseOptions, g as CallToolAsTaskOptions, m as TaskHandle, p as ToolCallResult, e as AgentAction, D as DataChangedEvent, R as RequestFileOptions, F as FileResult, b as ActionReducer, d as Store, k as StoreDispatch, a as Synapse, l as SynapseTheme } from '../types-Beo6hjvb.cjs'; | ||
| import { C as ConnectOptions, A as App, o as Theme, q as ToolResultData, S as SynapseOptions, g as CallToolAsTaskOptions, m as TaskHandle, p as ToolCallResult, e as AgentAction, D as DataChangedEvent, R as RequestFileOptions, F as FileResult, b as ActionReducer, d as Store, k as StoreDispatch, a as Synapse, l as SynapseTheme } from '../types-C9utGXv_.cjs'; | ||
| import { McpUiHostContext } from '@modelcontextprotocol/ext-apps'; | ||
@@ -5,0 +5,0 @@ import { Task } from '@modelcontextprotocol/sdk/types.js'; |
| import * as react_jsx_runtime from 'react/jsx-runtime'; | ||
| import { ReactNode } from 'react'; | ||
| import { C as ConnectOptions, A as App, o as Theme, q as ToolResultData, S as SynapseOptions, g as CallToolAsTaskOptions, m as TaskHandle, p as ToolCallResult, e as AgentAction, D as DataChangedEvent, R as RequestFileOptions, F as FileResult, b as ActionReducer, d as Store, k as StoreDispatch, a as Synapse, l as SynapseTheme } from '../types-Beo6hjvb.js'; | ||
| import { C as ConnectOptions, A as App, o as Theme, q as ToolResultData, S as SynapseOptions, g as CallToolAsTaskOptions, m as TaskHandle, p as ToolCallResult, e as AgentAction, D as DataChangedEvent, R as RequestFileOptions, F as FileResult, b as ActionReducer, d as Store, k as StoreDispatch, a as Synapse, l as SynapseTheme } from '../types-C9utGXv_.js'; | ||
| import { McpUiHostContext } from '@modelcontextprotocol/ext-apps'; | ||
@@ -5,0 +5,0 @@ import { Task } from '@modelcontextprotocol/sdk/types.js'; |
@@ -1,2 +0,2 @@ | ||
| import { connect, createSynapse } from '../chunk-2HKN2A2J.js'; | ||
| import { connect, createSynapse } from '../chunk-IQYOVYIR.js'; | ||
| import { createContext, useState, useRef, useEffect, useCallback, useSyncExternalStore, useContext } from 'react'; | ||
@@ -3,0 +3,0 @@ import { jsx, jsxs } from 'react/jsx-runtime'; |
+1
-1
| { | ||
| "name": "@nimblebrain/synapse", | ||
| "version": "0.7.0", | ||
| "version": "0.8.0", | ||
| "description": "Agent-aware app SDK for the MCP ext-apps protocol", | ||
@@ -5,0 +5,0 @@ "type": "module", |
| import { INITIALIZE_METHOD, LATEST_PROTOCOL_VERSION, HOST_CONTEXT_CHANGED_METHOD, INITIALIZED_METHOD, MESSAGE_METHOD, OPEN_LINK_METHOD, TOOL_RESULT_METHOD, RESOURCE_TEARDOWN_METHOD, TOOL_CANCELLED_METHOD, TOOL_INPUT_PARTIAL_METHOD, TOOL_INPUT_METHOD } from '@modelcontextprotocol/ext-apps'; | ||
| // src/connect.ts | ||
| // src/content-parser.ts | ||
| function parseToolResultParams(params) { | ||
| const raw = params ?? {}; | ||
| const structuredContent = raw.structuredContent ?? null; | ||
| if (structuredContent != null) { | ||
| return { content: structuredContent, structuredContent, raw }; | ||
| } | ||
| const rawContent = raw.content; | ||
| if (Array.isArray(rawContent)) { | ||
| const texts = rawContent.filter( | ||
| (block) => block != null && typeof block === "object" && block.type === "text" && typeof block.text === "string" | ||
| ).map((block) => block.text); | ||
| if (texts.length > 0) { | ||
| const joined = texts.join(""); | ||
| try { | ||
| return { content: JSON.parse(joined), structuredContent: null, raw }; | ||
| } catch { | ||
| return { content: joined, structuredContent: null, raw }; | ||
| } | ||
| } | ||
| return { content: rawContent, structuredContent: null, raw }; | ||
| } | ||
| if (typeof rawContent === "string") { | ||
| try { | ||
| return { content: JSON.parse(rawContent), structuredContent: null, raw }; | ||
| } catch { | ||
| return { content: rawContent, structuredContent: null, raw }; | ||
| } | ||
| } | ||
| return { content: rawContent ?? null, structuredContent: null, raw }; | ||
| } | ||
| var EVENT_MAP = { | ||
| "tool-result": TOOL_RESULT_METHOD, | ||
| "tool-input": TOOL_INPUT_METHOD, | ||
| "tool-input-partial": TOOL_INPUT_PARTIAL_METHOD, | ||
| "tool-cancelled": TOOL_CANCELLED_METHOD, | ||
| "theme-changed": HOST_CONTEXT_CHANGED_METHOD, | ||
| teardown: RESOURCE_TEARDOWN_METHOD | ||
| }; | ||
| function resolveEventMethod(name) { | ||
| return EVENT_MAP[name] ?? name; | ||
| } | ||
| // src/resize.ts | ||
| function createResizer(send, autoResize) { | ||
| let destroyed = false; | ||
| let observer = null; | ||
| let rafId = null; | ||
| function measureAndSend() { | ||
| if (destroyed) return; | ||
| const width = document.body.scrollWidth; | ||
| const height = document.body.scrollHeight; | ||
| send("ui/notifications/size-changed", { width, height }); | ||
| } | ||
| function resize(width, height) { | ||
| if (destroyed) return; | ||
| if (width !== void 0 && height !== void 0) { | ||
| send("ui/notifications/size-changed", { width, height }); | ||
| } else { | ||
| measureAndSend(); | ||
| } | ||
| } | ||
| if (autoResize && typeof ResizeObserver !== "undefined") { | ||
| observer = new ResizeObserver(() => { | ||
| if (destroyed) return; | ||
| if (rafId !== null) cancelAnimationFrame(rafId); | ||
| rafId = requestAnimationFrame(() => { | ||
| rafId = null; | ||
| measureAndSend(); | ||
| }); | ||
| }); | ||
| observer.observe(document.body); | ||
| } | ||
| function destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| if (rafId !== null) cancelAnimationFrame(rafId); | ||
| observer?.disconnect(); | ||
| observer = null; | ||
| } | ||
| return { resize, measureAndSend, destroy }; | ||
| } | ||
| // src/result-parser.ts | ||
| function parseToolResult(raw) { | ||
| if (raw == null) { | ||
| return { data: null, isError: false }; | ||
| } | ||
| if (isCallToolResult(raw)) { | ||
| return parseCallToolResult(raw); | ||
| } | ||
| return { data: raw, isError: false }; | ||
| } | ||
| function isCallToolResult(value) { | ||
| if (value === null || typeof value !== "object" || Array.isArray(value)) { | ||
| return false; | ||
| } | ||
| return Array.isArray(value.content); | ||
| } | ||
| function isTextBlock(block) { | ||
| if (block === null || typeof block !== "object" || Array.isArray(block)) { | ||
| return false; | ||
| } | ||
| const obj = block; | ||
| return obj.type === "text" && typeof obj.text === "string"; | ||
| } | ||
| function extractMeta(result) { | ||
| const meta = result._meta; | ||
| if (!meta || typeof meta !== "object" || Array.isArray(meta)) return void 0; | ||
| return { ...meta }; | ||
| } | ||
| function parseCallToolResult(result) { | ||
| const isError = result.isError === true; | ||
| const content = result.content; | ||
| const meta = extractMeta(result); | ||
| if (content.length === 0) { | ||
| return { data: null, isError, content, ...meta && { _meta: meta } }; | ||
| } | ||
| const firstText = content.find(isTextBlock); | ||
| if (!firstText) { | ||
| return { data: content, isError, content, ...meta && { _meta: meta } }; | ||
| } | ||
| try { | ||
| return { data: JSON.parse(firstText.text), isError, content, ...meta && { _meta: meta } }; | ||
| } catch { | ||
| return { data: firstText.text, isError, content, ...meta && { _meta: meta } }; | ||
| } | ||
| } | ||
| // src/transport.ts | ||
| var SynapseTransport = class { | ||
| counter = 0; | ||
| destroyed = false; | ||
| pending = /* @__PURE__ */ new Map(); | ||
| handlers = /* @__PURE__ */ new Map(); | ||
| listener; | ||
| constructor() { | ||
| this.listener = (event) => this.handleMessage(event); | ||
| window.addEventListener("message", this.listener); | ||
| } | ||
| send(method, params) { | ||
| if (this.destroyed) return; | ||
| const msg = { | ||
| jsonrpc: "2.0", | ||
| method, | ||
| ...params !== void 0 && { params } | ||
| }; | ||
| window.parent.postMessage(msg, "*"); | ||
| } | ||
| request(method, params) { | ||
| if (this.destroyed) { | ||
| return Promise.reject(new Error("Transport destroyed")); | ||
| } | ||
| const id = `syn-${++this.counter}`; | ||
| const msg = { | ||
| jsonrpc: "2.0", | ||
| method, | ||
| id, | ||
| ...params !== void 0 && { params } | ||
| }; | ||
| return new Promise((resolve, reject) => { | ||
| this.pending.set(id, { resolve, reject }); | ||
| window.parent.postMessage(msg, "*"); | ||
| }); | ||
| } | ||
| onMessage(method, callback) { | ||
| if (!this.handlers.has(method)) { | ||
| this.handlers.set(method, /* @__PURE__ */ new Set()); | ||
| } | ||
| this.handlers.get(method)?.add(callback); | ||
| return () => { | ||
| const set = this.handlers.get(method); | ||
| if (set) { | ||
| set.delete(callback); | ||
| if (set.size === 0) { | ||
| this.handlers.delete(method); | ||
| } | ||
| } | ||
| }; | ||
| } | ||
| destroy() { | ||
| if (this.destroyed) return; | ||
| this.destroyed = true; | ||
| window.removeEventListener("message", this.listener); | ||
| const error = new Error("Transport destroyed"); | ||
| for (const entry of this.pending.values()) { | ||
| entry.reject(error); | ||
| } | ||
| this.pending.clear(); | ||
| this.handlers.clear(); | ||
| } | ||
| handleMessage(event) { | ||
| if (this.destroyed) return; | ||
| const data = event.data; | ||
| if (!data || data.jsonrpc !== "2.0") return; | ||
| if ("id" in data && data.id && !("method" in data)) { | ||
| const response = data; | ||
| const entry = this.pending.get(response.id); | ||
| if (!entry) return; | ||
| this.pending.delete(response.id); | ||
| if (response.error) { | ||
| const err = new Error(response.error.message); | ||
| err.code = response.error.code; | ||
| err.data = response.error.data; | ||
| entry.reject(err); | ||
| } else { | ||
| entry.resolve(response.result); | ||
| } | ||
| return; | ||
| } | ||
| if ("method" in data && !("id" in data && data.id)) { | ||
| const notification = data; | ||
| const set = this.handlers.get(notification.method); | ||
| if (set) { | ||
| for (const handler of set) { | ||
| handler(notification.params); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }; | ||
| // src/connect.ts | ||
| var READ_RESOURCE_METHOD = "resources/read"; | ||
| async function connect(options) { | ||
| const { name, version, autoResize = false } = options; | ||
| const transport = new SynapseTransport(); | ||
| let destroyed = false; | ||
| let currentTheme = { mode: "light", tokens: {} }; | ||
| let hostInfo = { name: "unknown", version: "unknown" }; | ||
| let toolInfo = null; | ||
| let containerDimensions = null; | ||
| const handlers = /* @__PURE__ */ new Map(); | ||
| const resizer = createResizer((method, params) => transport.send(method, params), autoResize); | ||
| resizer.measureAndSend(); | ||
| const initParams = { | ||
| protocolVersion: LATEST_PROTOCOL_VERSION, | ||
| appInfo: { name, version }, | ||
| appCapabilities: {} | ||
| }; | ||
| const result = await transport.request( | ||
| INITIALIZE_METHOD, | ||
| initParams | ||
| ); | ||
| if (result) { | ||
| hostInfo = { | ||
| name: result.hostInfo?.name ?? "unknown", | ||
| version: result.hostInfo?.version ?? "unknown" | ||
| }; | ||
| const ctx = result.hostContext; | ||
| if (ctx) { | ||
| currentTheme = { | ||
| mode: ctx.theme === "dark" ? "dark" : "light", | ||
| tokens: ctx.styles?.variables && typeof ctx.styles.variables === "object" ? ctx.styles.variables : {} | ||
| }; | ||
| if (ctx.toolInfo && typeof ctx.toolInfo === "object") { | ||
| toolInfo = { tool: ctx.toolInfo.tool ?? {} }; | ||
| } | ||
| if (ctx.containerDimensions && typeof ctx.containerDimensions === "object") { | ||
| containerDimensions = ctx.containerDimensions; | ||
| } | ||
| injectCssVariables(ctx.styles?.variables); | ||
| } | ||
| } | ||
| transport.onMessage(HOST_CONTEXT_CHANGED_METHOD, (params) => { | ||
| if (destroyed || !params) return; | ||
| const ctx = params; | ||
| const mode = ctx.theme === "dark" ? "dark" : "light"; | ||
| const variables = ctx.styles?.variables; | ||
| const tokens = variables && typeof variables === "object" ? variables : currentTheme.tokens; | ||
| currentTheme = { mode, tokens }; | ||
| injectCssVariables(tokens); | ||
| const set = handlers.get(HOST_CONTEXT_CHANGED_METHOD); | ||
| if (set) { | ||
| for (const handler of set) handler(currentTheme); | ||
| } | ||
| }); | ||
| const subscribedMethods = /* @__PURE__ */ new Set([HOST_CONTEXT_CHANGED_METHOD]); | ||
| function ensureTransportSub(method) { | ||
| if (subscribedMethods.has(method)) return; | ||
| subscribedMethods.add(method); | ||
| const isToolResult = method === TOOL_RESULT_METHOD; | ||
| transport.onMessage(method, (params) => { | ||
| if (destroyed) return; | ||
| const set = handlers.get(method); | ||
| if (!set) return; | ||
| for (const handler of set) { | ||
| if (isToolResult) { | ||
| handler(parseToolResultParams(params)); | ||
| } else { | ||
| handler(params); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| if (options.on) { | ||
| for (const [event, handler] of Object.entries(options.on)) { | ||
| if (typeof handler === "function") { | ||
| const method = resolveEventMethod(event); | ||
| if (!handlers.has(method)) handlers.set(method, /* @__PURE__ */ new Set()); | ||
| handlers.get(method)?.add(handler); | ||
| ensureTransportSub(method); | ||
| } | ||
| } | ||
| } | ||
| transport.send(INITIALIZED_METHOD, {}); | ||
| const app = { | ||
| get theme() { | ||
| return { ...currentTheme }; | ||
| }, | ||
| get hostInfo() { | ||
| return { ...hostInfo }; | ||
| }, | ||
| get toolInfo() { | ||
| return toolInfo; | ||
| }, | ||
| get containerDimensions() { | ||
| return containerDimensions; | ||
| }, | ||
| on(event, handler) { | ||
| const method = resolveEventMethod(event); | ||
| if (!handlers.has(method)) { | ||
| handlers.set(method, /* @__PURE__ */ new Set()); | ||
| } | ||
| handlers.get(method)?.add(handler); | ||
| ensureTransportSub(method); | ||
| return () => { | ||
| const set = handlers.get(method); | ||
| if (set) { | ||
| set.delete(handler); | ||
| if (set.size === 0) handlers.delete(method); | ||
| } | ||
| }; | ||
| }, | ||
| resize(width, height) { | ||
| resizer.resize(width, height); | ||
| }, | ||
| openLink(url) { | ||
| if (destroyed) return; | ||
| const params = { url }; | ||
| transport.request(OPEN_LINK_METHOD, params).catch(() => { | ||
| }); | ||
| }, | ||
| updateModelContext(state, summary) { | ||
| if (destroyed) return; | ||
| const params = { | ||
| structuredContent: state, | ||
| ...summary !== void 0 && { | ||
| content: [{ type: "text", text: summary }] | ||
| } | ||
| }; | ||
| transport.send("ui/update-model-context", params); | ||
| }, | ||
| async callTool(toolName, args) { | ||
| const params = { | ||
| name: toolName, | ||
| arguments: args ?? {} | ||
| }; | ||
| const raw = await transport.request( | ||
| "tools/call", | ||
| params | ||
| ); | ||
| return parseToolResult(raw); | ||
| }, | ||
| async readServerResource(params) { | ||
| const raw = await transport.request( | ||
| READ_RESOURCE_METHOD, | ||
| params | ||
| ); | ||
| return raw; | ||
| }, | ||
| sendMessage(text, context) { | ||
| if (destroyed) return; | ||
| const textBlock = { | ||
| type: "text", | ||
| text, | ||
| ...context && { _meta: { context } } | ||
| }; | ||
| const params = { | ||
| role: "user", | ||
| content: [textBlock] | ||
| }; | ||
| transport.send(MESSAGE_METHOD, params); | ||
| }, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| resizer.destroy(); | ||
| handlers.clear(); | ||
| transport.destroy(); | ||
| } | ||
| }; | ||
| return app; | ||
| } | ||
| function injectCssVariables(vars) { | ||
| if (!vars || typeof vars !== "object") return; | ||
| for (const [k, v] of Object.entries(vars)) { | ||
| if (typeof k === "string" && typeof v === "string") { | ||
| document.documentElement.style.setProperty(k, v); | ||
| } | ||
| } | ||
| } | ||
| // src/detection.ts | ||
| var DEFAULT_THEME = { | ||
| mode: "light", | ||
| primaryColor: "#6366f1", | ||
| tokens: {} | ||
| }; | ||
| function detectHost(initResponse) { | ||
| const resp = initResponse; | ||
| const hostName = resp?.hostInfo?.name ?? "unknown"; | ||
| const protocolVersion = resp?.protocolVersion ?? "unknown"; | ||
| return { | ||
| isNimbleBrain: hostName === "nimblebrain", | ||
| serverName: hostName, | ||
| protocolVersion | ||
| }; | ||
| } | ||
| function extractTheme(ctx) { | ||
| if (!ctx) return { ...DEFAULT_THEME }; | ||
| const mode = ctx.theme === "light" || ctx.theme === "dark" ? ctx.theme : DEFAULT_THEME.mode; | ||
| const variables = ctx.styles?.variables; | ||
| const tokens = variables && typeof variables === "object" && !Array.isArray(variables) ? variables : {}; | ||
| return { mode, primaryColor: DEFAULT_THEME.primaryColor, tokens }; | ||
| } | ||
| // src/keyboard.ts | ||
| var KeyboardForwarder = class { | ||
| listener; | ||
| destroyed = false; | ||
| constructor(transport, customKeys) { | ||
| const config = customKeys ?? null; | ||
| this.listener = (event) => { | ||
| if (this.destroyed) return; | ||
| if (this.shouldForward(event, config)) { | ||
| event.preventDefault(); | ||
| transport.send("synapse/keydown", { | ||
| key: event.key, | ||
| ctrlKey: event.ctrlKey, | ||
| metaKey: event.metaKey, | ||
| shiftKey: event.shiftKey, | ||
| altKey: event.altKey | ||
| }); | ||
| } | ||
| }; | ||
| document.addEventListener("keydown", this.listener); | ||
| } | ||
| destroy() { | ||
| if (this.destroyed) return; | ||
| this.destroyed = true; | ||
| document.removeEventListener("keydown", this.listener); | ||
| } | ||
| shouldForward(event, config) { | ||
| if (config && config.length === 0) return false; | ||
| if (config) { | ||
| return config.some( | ||
| (k) => event.key.toLowerCase() === k.key.toLowerCase() && (k.ctrl === void 0 || event.ctrlKey === k.ctrl) && (k.meta === void 0 || event.metaKey === k.meta) && (k.shift === void 0 || event.shiftKey === k.shift) && (k.alt === void 0 || event.altKey === k.alt) | ||
| ); | ||
| } | ||
| if (event.key === "Escape") return true; | ||
| if (event.ctrlKey || event.metaKey) { | ||
| const key = event.key.toLowerCase(); | ||
| if (key === "c" || key === "v" || key === "x" || key === "a") return false; | ||
| return true; | ||
| } | ||
| return false; | ||
| } | ||
| }; | ||
| // src/task-handle.ts | ||
| var TOOLS_CALL_METHOD = "tools/call"; | ||
| var TASKS_GET_METHOD = "tasks/get"; | ||
| var TASKS_RESULT_METHOD = "tasks/result"; | ||
| var TASKS_CANCEL_METHOD = "tasks/cancel"; | ||
| var TASKS_STATUS_NOTIFICATION_METHOD = "notifications/tasks/status"; | ||
| function createTaskStatusRouter(transport) { | ||
| const listeners = /* @__PURE__ */ new Map(); | ||
| const unsub = transport.onMessage(TASKS_STATUS_NOTIFICATION_METHOD, (rawParams) => { | ||
| if (!rawParams) return; | ||
| const params = rawParams; | ||
| const taskId = params.taskId; | ||
| if (typeof taskId !== "string") return; | ||
| const set = listeners.get(taskId); | ||
| if (!set || set.size === 0) return; | ||
| const update = { | ||
| taskId: params.taskId, | ||
| status: params.status, | ||
| ...params.statusMessage !== void 0 && { statusMessage: params.statusMessage } | ||
| }; | ||
| for (const cb of set) cb(update); | ||
| }); | ||
| return { | ||
| subscribe(taskId, cb) { | ||
| let set = listeners.get(taskId); | ||
| if (!set) { | ||
| set = /* @__PURE__ */ new Set(); | ||
| listeners.set(taskId, set); | ||
| } | ||
| set.add(cb); | ||
| return () => { | ||
| const s = listeners.get(taskId); | ||
| if (!s) return; | ||
| s.delete(cb); | ||
| if (s.size === 0) listeners.delete(taskId); | ||
| }; | ||
| }, | ||
| dispose() { | ||
| listeners.clear(); | ||
| unsub(); | ||
| } | ||
| }; | ||
| } | ||
| async function callToolAsTask(deps, toolName, args, options) { | ||
| const hostTasks = deps.getHostTasksCapability(); | ||
| if (!hostTasks?.requests?.tools?.call) { | ||
| throw new Error( | ||
| "callToolAsTask: host did not advertise tasks.requests.tools.call in its capabilities. Per MCP 2025-11-25 \xA7, requestors MUST NOT task-augment a tools/call without matching receiver capability. Fall back to `synapse.callTool`." | ||
| ); | ||
| } | ||
| const taskParam = {}; | ||
| if (options?.ttl !== void 0) taskParam.ttl = options.ttl; | ||
| const crossServer = options?.internal ?? deps.internalApp; | ||
| const callParams = { | ||
| name: toolName, | ||
| arguments: args ?? {}, | ||
| task: taskParam, | ||
| ...crossServer ? { server: deps.appName } : {} | ||
| }; | ||
| const raw = await deps.transport.request( | ||
| TOOLS_CALL_METHOD, | ||
| callParams | ||
| ); | ||
| const createResult = raw; | ||
| const initialTask = createResult?.task; | ||
| if (!initialTask || typeof initialTask !== "object" || typeof initialTask.taskId !== "string") { | ||
| throw new Error( | ||
| "callToolAsTask: receiver returned a response without `task` per CreateTaskResult (expected shape: `{ task: { taskId, status, ... } }`). Receiver may not honor the advertised tasks capability." | ||
| ); | ||
| } | ||
| const taskId = initialTask.taskId; | ||
| const localCallbacks = /* @__PURE__ */ new Map(); | ||
| const handle = { | ||
| task: initialTask, | ||
| async result() { | ||
| const params = { taskId }; | ||
| const rawResult = await deps.transport.request( | ||
| TASKS_RESULT_METHOD, | ||
| params | ||
| ); | ||
| const typed = rawResult; | ||
| return parseToolResult(typed); | ||
| }, | ||
| async refresh() { | ||
| const params = { taskId }; | ||
| const raw2 = await deps.transport.request( | ||
| TASKS_GET_METHOD, | ||
| params | ||
| ); | ||
| return projectTask(raw2); | ||
| }, | ||
| async cancel() { | ||
| const params = { taskId }; | ||
| const raw2 = await deps.transport.request( | ||
| TASKS_CANCEL_METHOD, | ||
| params | ||
| ); | ||
| return projectTask(raw2); | ||
| }, | ||
| onStatus(cb) { | ||
| const existing = localCallbacks.get(cb); | ||
| if (existing) return existing; | ||
| const wireUnsub = deps.router.subscribe(taskId, (update) => { | ||
| const merged = { | ||
| taskId: update.taskId, | ||
| status: update.status, | ||
| ttl: initialTask.ttl, | ||
| createdAt: initialTask.createdAt, | ||
| lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString(), | ||
| ...initialTask.pollInterval !== void 0 && { | ||
| pollInterval: initialTask.pollInterval | ||
| }, | ||
| ...update.statusMessage !== void 0 && { statusMessage: update.statusMessage } | ||
| }; | ||
| cb(merged); | ||
| }); | ||
| const unsub = () => { | ||
| localCallbacks.delete(cb); | ||
| wireUnsub(); | ||
| }; | ||
| localCallbacks.set(cb, unsub); | ||
| return unsub; | ||
| } | ||
| }; | ||
| return handle; | ||
| } | ||
| function projectTask(raw) { | ||
| return { | ||
| taskId: raw.taskId, | ||
| status: raw.status, | ||
| ttl: raw.ttl, | ||
| createdAt: raw.createdAt, | ||
| lastUpdatedAt: raw.lastUpdatedAt, | ||
| ...raw.pollInterval !== void 0 && { pollInterval: raw.pollInterval }, | ||
| ...raw.statusMessage !== void 0 && { statusMessage: raw.statusMessage } | ||
| }; | ||
| } | ||
| // src/core.ts | ||
| var READ_RESOURCE_METHOD2 = "resources/read"; | ||
| function createSynapse(options) { | ||
| const { name, version, internal = false, forwardKeys } = options; | ||
| const transport = new SynapseTransport(); | ||
| let hostInfo = null; | ||
| let currentHostContext = {}; | ||
| let hostTasksCapability = null; | ||
| let destroyed = false; | ||
| const taskStatusRouter = createTaskStatusRouter(transport); | ||
| let stateTimer = null; | ||
| let keyboard = null; | ||
| const appCapabilities = { | ||
| tasks: { | ||
| cancel: {}, | ||
| requests: { tools: { call: {} } } | ||
| } | ||
| }; | ||
| const initParams = { | ||
| protocolVersion: LATEST_PROTOCOL_VERSION, | ||
| appInfo: { name, version }, | ||
| appCapabilities | ||
| }; | ||
| const ready = transport.request(INITIALIZE_METHOD, initParams).then((result) => { | ||
| hostInfo = detectHost(result); | ||
| currentHostContext = result?.hostContext ?? {}; | ||
| const initResult = result; | ||
| const rawTasks = initResult?.hostCapabilities?.tasks; | ||
| hostTasksCapability = rawTasks && typeof rawTasks === "object" && !Array.isArray(rawTasks) ? rawTasks : void 0; | ||
| injectCssVariables2(extractTheme(currentHostContext).tokens); | ||
| for (const cb of hostContextCallbacks) cb(currentHostContext); | ||
| transport.send(INITIALIZED_METHOD, {}); | ||
| keyboard = new KeyboardForwarder(transport, forwardKeys); | ||
| }); | ||
| const unsubHostContext = transport.onMessage(HOST_CONTEXT_CHANGED_METHOD, (params) => { | ||
| currentHostContext = params ?? {}; | ||
| injectCssVariables2(extractTheme(currentHostContext).tokens); | ||
| for (const cb of hostContextCallbacks) cb(currentHostContext); | ||
| }); | ||
| const hostContextCallbacks = /* @__PURE__ */ new Set(); | ||
| const dataCallbacks = /* @__PURE__ */ new Set(); | ||
| const actionCallbacks = /* @__PURE__ */ new Set(); | ||
| const unsubData = transport.onMessage("synapse/data-changed", (params) => { | ||
| if (!params) return; | ||
| const event = { | ||
| source: "agent", | ||
| server: params.server ?? "", | ||
| tool: params.tool ?? "" | ||
| }; | ||
| for (const cb of dataCallbacks) cb(event); | ||
| }); | ||
| const unsubAction = transport.onMessage("synapse/action", (params) => { | ||
| if (!params || typeof params.type !== "string") return; | ||
| const action = { | ||
| type: params.type, | ||
| payload: params.payload ?? {}, | ||
| requiresConfirmation: params.requiresConfirmation === true, | ||
| label: typeof params.label === "string" ? params.label : void 0 | ||
| }; | ||
| for (const cb of actionCallbacks) cb(action); | ||
| }); | ||
| const isNB = () => hostInfo?.isNimbleBrain === true; | ||
| const synapse = { | ||
| get ready() { | ||
| return ready; | ||
| }, | ||
| get isNimbleBrainHost() { | ||
| return isNB(); | ||
| }, | ||
| get destroyed() { | ||
| return destroyed; | ||
| }, | ||
| async callTool(toolName, args) { | ||
| const params = { | ||
| name: toolName, | ||
| arguments: args ?? {} | ||
| }; | ||
| if (internal) { | ||
| params.server = name; | ||
| } | ||
| const raw = await transport.request("tools/call", params); | ||
| return parseToolResult(raw); | ||
| }, | ||
| async callToolAsTask(toolName, args, options2) { | ||
| return callToolAsTask( | ||
| { | ||
| transport, | ||
| router: taskStatusRouter, | ||
| getHostTasksCapability: () => hostTasksCapability, | ||
| appName: name, | ||
| internalApp: internal | ||
| }, | ||
| toolName, | ||
| args, | ||
| options2 | ||
| ); | ||
| }, | ||
| async readResource(uri) { | ||
| const params = { uri }; | ||
| const raw = await transport.request( | ||
| READ_RESOURCE_METHOD2, | ||
| params | ||
| ); | ||
| return raw; | ||
| }, | ||
| onDataChanged(callback) { | ||
| dataCallbacks.add(callback); | ||
| return () => { | ||
| dataCallbacks.delete(callback); | ||
| }; | ||
| }, | ||
| onAction(callback) { | ||
| actionCallbacks.add(callback); | ||
| return () => { | ||
| actionCallbacks.delete(callback); | ||
| }; | ||
| }, | ||
| getHostContext() { | ||
| return currentHostContext; | ||
| }, | ||
| onHostContextChanged(callback) { | ||
| hostContextCallbacks.add(callback); | ||
| return () => { | ||
| hostContextCallbacks.delete(callback); | ||
| }; | ||
| }, | ||
| getTheme() { | ||
| return extractTheme(currentHostContext); | ||
| }, | ||
| // Selector over `onHostContextChanged`: only fires when the *derived* | ||
| // theme actually changes, so theme subscribers don't see spurious | ||
| // updates when other host-context fields (e.g. workspace) change. | ||
| // | ||
| // Subscriber timing matters: | ||
| // - Subscribed BEFORE handshake: `prev = null` sentinel. The first | ||
| // fire (the handshake dispatch) always invokes the callback, | ||
| // even if the host's theme happens to derive to the default. | ||
| // Otherwise consumers using `onThemeChanged` as their init | ||
| // signal would silently miss it. | ||
| // - Subscribed AFTER handshake: `prev` is pre-seeded with the | ||
| // current derived theme, so a workspace-only `host-context-changed` | ||
| // notification correctly filters as a no-op. | ||
| onThemeChanged(callback) { | ||
| let prev = hostInfo !== null ? extractTheme(currentHostContext) : null; | ||
| const wrapped = (ctx) => { | ||
| const next = extractTheme(ctx); | ||
| if (prev !== null && themesEqual(prev, next)) return; | ||
| prev = next; | ||
| callback(next); | ||
| }; | ||
| hostContextCallbacks.add(wrapped); | ||
| return () => { | ||
| hostContextCallbacks.delete(wrapped); | ||
| }; | ||
| }, | ||
| action(action, params) { | ||
| if (!isNB()) return; | ||
| transport.send("synapse/action", { action, ...params }); | ||
| }, | ||
| chat(message, context) { | ||
| const textBlock = { | ||
| type: "text", | ||
| text: message, | ||
| ...isNB() && context && { _meta: { context } } | ||
| }; | ||
| const params = { | ||
| role: "user", | ||
| content: [textBlock] | ||
| }; | ||
| transport.send(MESSAGE_METHOD, params); | ||
| }, | ||
| setVisibleState(state, summary) { | ||
| if (stateTimer) clearTimeout(stateTimer); | ||
| stateTimer = setTimeout(() => { | ||
| const params = { | ||
| structuredContent: state, | ||
| ...summary !== void 0 && { | ||
| content: [{ type: "text", text: summary }] | ||
| } | ||
| }; | ||
| transport.send("ui/update-model-context", params); | ||
| stateTimer = null; | ||
| }, 250); | ||
| }, | ||
| downloadFile(filename, content, mimeType) { | ||
| const resolvedMime = mimeType || (content instanceof Blob ? content.type : "") || "application/octet-stream"; | ||
| const blob = content instanceof Blob ? content : new Blob([content], { type: resolvedMime }); | ||
| transport.send("synapse/download-file", { | ||
| data: blob, | ||
| filename, | ||
| mimeType: resolvedMime | ||
| }); | ||
| }, | ||
| openLink(url) { | ||
| const params = { url }; | ||
| transport.request(OPEN_LINK_METHOD, params).catch(() => { | ||
| window.open(url, "_blank", "noopener"); | ||
| }); | ||
| }, | ||
| async pickFile(options2) { | ||
| if (!isNB()) { | ||
| throw new Error("pickFile is not supported in this host"); | ||
| } | ||
| const result = await transport.request("synapse/pick-file", { | ||
| accept: options2?.accept, | ||
| maxSize: options2?.maxSize ?? 26214400, | ||
| multiple: false | ||
| }); | ||
| return result ?? null; | ||
| }, | ||
| async pickFiles(options2) { | ||
| if (!isNB()) { | ||
| throw new Error("pickFiles is not supported in this host"); | ||
| } | ||
| const result = await transport.request("synapse/pick-file", { | ||
| accept: options2?.accept, | ||
| maxSize: options2?.maxSize ?? 26214400, | ||
| multiple: true | ||
| }); | ||
| if (!result) return []; | ||
| return Array.isArray(result) ? result : [result]; | ||
| }, | ||
| _onMessage(method, callback) { | ||
| return transport.onMessage(method, callback); | ||
| }, | ||
| _request(method, params) { | ||
| return transport.request(method, params); | ||
| }, | ||
| get _hostTasksCapability() { | ||
| return hostTasksCapability; | ||
| }, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| if (stateTimer) clearTimeout(stateTimer); | ||
| keyboard?.destroy(); | ||
| unsubHostContext(); | ||
| unsubData(); | ||
| unsubAction(); | ||
| taskStatusRouter.dispose(); | ||
| hostContextCallbacks.clear(); | ||
| dataCallbacks.clear(); | ||
| actionCallbacks.clear(); | ||
| transport.destroy(); | ||
| } | ||
| }; | ||
| return synapse; | ||
| } | ||
| function themesEqual(a, b) { | ||
| if (a.mode !== b.mode || a.primaryColor !== b.primaryColor) return false; | ||
| const aKeys = Object.keys(a.tokens); | ||
| const bKeys = Object.keys(b.tokens); | ||
| if (aKeys.length !== bKeys.length) return false; | ||
| for (const k of aKeys) { | ||
| if (a.tokens[k] !== b.tokens[k]) return false; | ||
| } | ||
| return true; | ||
| } | ||
| function injectCssVariables2(vars) { | ||
| if (!vars || typeof vars !== "object") return; | ||
| if (typeof document === "undefined") return; | ||
| for (const [k, v] of Object.entries(vars)) { | ||
| if (typeof k === "string" && typeof v === "string") { | ||
| document.documentElement.style.setProperty(k, v); | ||
| } | ||
| } | ||
| } | ||
| export { connect, createSynapse }; | ||
| //# sourceMappingURL=chunk-2HKN2A2J.js.map | ||
| //# sourceMappingURL=chunk-2HKN2A2J.js.map |
Sorry, the diff of this file is too big to display
| 'use strict'; | ||
| var extApps = require('@modelcontextprotocol/ext-apps'); | ||
| // src/connect.ts | ||
| // src/content-parser.ts | ||
| function parseToolResultParams(params) { | ||
| const raw = params ?? {}; | ||
| const structuredContent = raw.structuredContent ?? null; | ||
| if (structuredContent != null) { | ||
| return { content: structuredContent, structuredContent, raw }; | ||
| } | ||
| const rawContent = raw.content; | ||
| if (Array.isArray(rawContent)) { | ||
| const texts = rawContent.filter( | ||
| (block) => block != null && typeof block === "object" && block.type === "text" && typeof block.text === "string" | ||
| ).map((block) => block.text); | ||
| if (texts.length > 0) { | ||
| const joined = texts.join(""); | ||
| try { | ||
| return { content: JSON.parse(joined), structuredContent: null, raw }; | ||
| } catch { | ||
| return { content: joined, structuredContent: null, raw }; | ||
| } | ||
| } | ||
| return { content: rawContent, structuredContent: null, raw }; | ||
| } | ||
| if (typeof rawContent === "string") { | ||
| try { | ||
| return { content: JSON.parse(rawContent), structuredContent: null, raw }; | ||
| } catch { | ||
| return { content: rawContent, structuredContent: null, raw }; | ||
| } | ||
| } | ||
| return { content: rawContent ?? null, structuredContent: null, raw }; | ||
| } | ||
| var EVENT_MAP = { | ||
| "tool-result": extApps.TOOL_RESULT_METHOD, | ||
| "tool-input": extApps.TOOL_INPUT_METHOD, | ||
| "tool-input-partial": extApps.TOOL_INPUT_PARTIAL_METHOD, | ||
| "tool-cancelled": extApps.TOOL_CANCELLED_METHOD, | ||
| "theme-changed": extApps.HOST_CONTEXT_CHANGED_METHOD, | ||
| teardown: extApps.RESOURCE_TEARDOWN_METHOD | ||
| }; | ||
| function resolveEventMethod(name) { | ||
| return EVENT_MAP[name] ?? name; | ||
| } | ||
| // src/resize.ts | ||
| function createResizer(send, autoResize) { | ||
| let destroyed = false; | ||
| let observer = null; | ||
| let rafId = null; | ||
| function measureAndSend() { | ||
| if (destroyed) return; | ||
| const width = document.body.scrollWidth; | ||
| const height = document.body.scrollHeight; | ||
| send("ui/notifications/size-changed", { width, height }); | ||
| } | ||
| function resize(width, height) { | ||
| if (destroyed) return; | ||
| if (width !== void 0 && height !== void 0) { | ||
| send("ui/notifications/size-changed", { width, height }); | ||
| } else { | ||
| measureAndSend(); | ||
| } | ||
| } | ||
| if (autoResize && typeof ResizeObserver !== "undefined") { | ||
| observer = new ResizeObserver(() => { | ||
| if (destroyed) return; | ||
| if (rafId !== null) cancelAnimationFrame(rafId); | ||
| rafId = requestAnimationFrame(() => { | ||
| rafId = null; | ||
| measureAndSend(); | ||
| }); | ||
| }); | ||
| observer.observe(document.body); | ||
| } | ||
| function destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| if (rafId !== null) cancelAnimationFrame(rafId); | ||
| observer?.disconnect(); | ||
| observer = null; | ||
| } | ||
| return { resize, measureAndSend, destroy }; | ||
| } | ||
| // src/result-parser.ts | ||
| function parseToolResult(raw) { | ||
| if (raw == null) { | ||
| return { data: null, isError: false }; | ||
| } | ||
| if (isCallToolResult(raw)) { | ||
| return parseCallToolResult(raw); | ||
| } | ||
| return { data: raw, isError: false }; | ||
| } | ||
| function isCallToolResult(value) { | ||
| if (value === null || typeof value !== "object" || Array.isArray(value)) { | ||
| return false; | ||
| } | ||
| return Array.isArray(value.content); | ||
| } | ||
| function isTextBlock(block) { | ||
| if (block === null || typeof block !== "object" || Array.isArray(block)) { | ||
| return false; | ||
| } | ||
| const obj = block; | ||
| return obj.type === "text" && typeof obj.text === "string"; | ||
| } | ||
| function extractMeta(result) { | ||
| const meta = result._meta; | ||
| if (!meta || typeof meta !== "object" || Array.isArray(meta)) return void 0; | ||
| return { ...meta }; | ||
| } | ||
| function parseCallToolResult(result) { | ||
| const isError = result.isError === true; | ||
| const content = result.content; | ||
| const meta = extractMeta(result); | ||
| if (content.length === 0) { | ||
| return { data: null, isError, content, ...meta && { _meta: meta } }; | ||
| } | ||
| const firstText = content.find(isTextBlock); | ||
| if (!firstText) { | ||
| return { data: content, isError, content, ...meta && { _meta: meta } }; | ||
| } | ||
| try { | ||
| return { data: JSON.parse(firstText.text), isError, content, ...meta && { _meta: meta } }; | ||
| } catch { | ||
| return { data: firstText.text, isError, content, ...meta && { _meta: meta } }; | ||
| } | ||
| } | ||
| // src/transport.ts | ||
| var SynapseTransport = class { | ||
| counter = 0; | ||
| destroyed = false; | ||
| pending = /* @__PURE__ */ new Map(); | ||
| handlers = /* @__PURE__ */ new Map(); | ||
| listener; | ||
| constructor() { | ||
| this.listener = (event) => this.handleMessage(event); | ||
| window.addEventListener("message", this.listener); | ||
| } | ||
| send(method, params) { | ||
| if (this.destroyed) return; | ||
| const msg = { | ||
| jsonrpc: "2.0", | ||
| method, | ||
| ...params !== void 0 && { params } | ||
| }; | ||
| window.parent.postMessage(msg, "*"); | ||
| } | ||
| request(method, params) { | ||
| if (this.destroyed) { | ||
| return Promise.reject(new Error("Transport destroyed")); | ||
| } | ||
| const id = `syn-${++this.counter}`; | ||
| const msg = { | ||
| jsonrpc: "2.0", | ||
| method, | ||
| id, | ||
| ...params !== void 0 && { params } | ||
| }; | ||
| return new Promise((resolve, reject) => { | ||
| this.pending.set(id, { resolve, reject }); | ||
| window.parent.postMessage(msg, "*"); | ||
| }); | ||
| } | ||
| onMessage(method, callback) { | ||
| if (!this.handlers.has(method)) { | ||
| this.handlers.set(method, /* @__PURE__ */ new Set()); | ||
| } | ||
| this.handlers.get(method)?.add(callback); | ||
| return () => { | ||
| const set = this.handlers.get(method); | ||
| if (set) { | ||
| set.delete(callback); | ||
| if (set.size === 0) { | ||
| this.handlers.delete(method); | ||
| } | ||
| } | ||
| }; | ||
| } | ||
| destroy() { | ||
| if (this.destroyed) return; | ||
| this.destroyed = true; | ||
| window.removeEventListener("message", this.listener); | ||
| const error = new Error("Transport destroyed"); | ||
| for (const entry of this.pending.values()) { | ||
| entry.reject(error); | ||
| } | ||
| this.pending.clear(); | ||
| this.handlers.clear(); | ||
| } | ||
| handleMessage(event) { | ||
| if (this.destroyed) return; | ||
| const data = event.data; | ||
| if (!data || data.jsonrpc !== "2.0") return; | ||
| if ("id" in data && data.id && !("method" in data)) { | ||
| const response = data; | ||
| const entry = this.pending.get(response.id); | ||
| if (!entry) return; | ||
| this.pending.delete(response.id); | ||
| if (response.error) { | ||
| const err = new Error(response.error.message); | ||
| err.code = response.error.code; | ||
| err.data = response.error.data; | ||
| entry.reject(err); | ||
| } else { | ||
| entry.resolve(response.result); | ||
| } | ||
| return; | ||
| } | ||
| if ("method" in data && !("id" in data && data.id)) { | ||
| const notification = data; | ||
| const set = this.handlers.get(notification.method); | ||
| if (set) { | ||
| for (const handler of set) { | ||
| handler(notification.params); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| }; | ||
| // src/connect.ts | ||
| var READ_RESOURCE_METHOD = "resources/read"; | ||
| async function connect(options) { | ||
| const { name, version, autoResize = false } = options; | ||
| const transport = new SynapseTransport(); | ||
| let destroyed = false; | ||
| let currentTheme = { mode: "light", tokens: {} }; | ||
| let hostInfo = { name: "unknown", version: "unknown" }; | ||
| let toolInfo = null; | ||
| let containerDimensions = null; | ||
| const handlers = /* @__PURE__ */ new Map(); | ||
| const resizer = createResizer((method, params) => transport.send(method, params), autoResize); | ||
| resizer.measureAndSend(); | ||
| const initParams = { | ||
| protocolVersion: extApps.LATEST_PROTOCOL_VERSION, | ||
| appInfo: { name, version }, | ||
| appCapabilities: {} | ||
| }; | ||
| const result = await transport.request( | ||
| extApps.INITIALIZE_METHOD, | ||
| initParams | ||
| ); | ||
| if (result) { | ||
| hostInfo = { | ||
| name: result.hostInfo?.name ?? "unknown", | ||
| version: result.hostInfo?.version ?? "unknown" | ||
| }; | ||
| const ctx = result.hostContext; | ||
| if (ctx) { | ||
| currentTheme = { | ||
| mode: ctx.theme === "dark" ? "dark" : "light", | ||
| tokens: ctx.styles?.variables && typeof ctx.styles.variables === "object" ? ctx.styles.variables : {} | ||
| }; | ||
| if (ctx.toolInfo && typeof ctx.toolInfo === "object") { | ||
| toolInfo = { tool: ctx.toolInfo.tool ?? {} }; | ||
| } | ||
| if (ctx.containerDimensions && typeof ctx.containerDimensions === "object") { | ||
| containerDimensions = ctx.containerDimensions; | ||
| } | ||
| injectCssVariables(ctx.styles?.variables); | ||
| } | ||
| } | ||
| transport.onMessage(extApps.HOST_CONTEXT_CHANGED_METHOD, (params) => { | ||
| if (destroyed || !params) return; | ||
| const ctx = params; | ||
| const mode = ctx.theme === "dark" ? "dark" : "light"; | ||
| const variables = ctx.styles?.variables; | ||
| const tokens = variables && typeof variables === "object" ? variables : currentTheme.tokens; | ||
| currentTheme = { mode, tokens }; | ||
| injectCssVariables(tokens); | ||
| const set = handlers.get(extApps.HOST_CONTEXT_CHANGED_METHOD); | ||
| if (set) { | ||
| for (const handler of set) handler(currentTheme); | ||
| } | ||
| }); | ||
| const subscribedMethods = /* @__PURE__ */ new Set([extApps.HOST_CONTEXT_CHANGED_METHOD]); | ||
| function ensureTransportSub(method) { | ||
| if (subscribedMethods.has(method)) return; | ||
| subscribedMethods.add(method); | ||
| const isToolResult = method === extApps.TOOL_RESULT_METHOD; | ||
| transport.onMessage(method, (params) => { | ||
| if (destroyed) return; | ||
| const set = handlers.get(method); | ||
| if (!set) return; | ||
| for (const handler of set) { | ||
| if (isToolResult) { | ||
| handler(parseToolResultParams(params)); | ||
| } else { | ||
| handler(params); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| if (options.on) { | ||
| for (const [event, handler] of Object.entries(options.on)) { | ||
| if (typeof handler === "function") { | ||
| const method = resolveEventMethod(event); | ||
| if (!handlers.has(method)) handlers.set(method, /* @__PURE__ */ new Set()); | ||
| handlers.get(method)?.add(handler); | ||
| ensureTransportSub(method); | ||
| } | ||
| } | ||
| } | ||
| transport.send(extApps.INITIALIZED_METHOD, {}); | ||
| const app = { | ||
| get theme() { | ||
| return { ...currentTheme }; | ||
| }, | ||
| get hostInfo() { | ||
| return { ...hostInfo }; | ||
| }, | ||
| get toolInfo() { | ||
| return toolInfo; | ||
| }, | ||
| get containerDimensions() { | ||
| return containerDimensions; | ||
| }, | ||
| on(event, handler) { | ||
| const method = resolveEventMethod(event); | ||
| if (!handlers.has(method)) { | ||
| handlers.set(method, /* @__PURE__ */ new Set()); | ||
| } | ||
| handlers.get(method)?.add(handler); | ||
| ensureTransportSub(method); | ||
| return () => { | ||
| const set = handlers.get(method); | ||
| if (set) { | ||
| set.delete(handler); | ||
| if (set.size === 0) handlers.delete(method); | ||
| } | ||
| }; | ||
| }, | ||
| resize(width, height) { | ||
| resizer.resize(width, height); | ||
| }, | ||
| openLink(url) { | ||
| if (destroyed) return; | ||
| const params = { url }; | ||
| transport.request(extApps.OPEN_LINK_METHOD, params).catch(() => { | ||
| }); | ||
| }, | ||
| updateModelContext(state, summary) { | ||
| if (destroyed) return; | ||
| const params = { | ||
| structuredContent: state, | ||
| ...summary !== void 0 && { | ||
| content: [{ type: "text", text: summary }] | ||
| } | ||
| }; | ||
| transport.send("ui/update-model-context", params); | ||
| }, | ||
| async callTool(toolName, args) { | ||
| const params = { | ||
| name: toolName, | ||
| arguments: args ?? {} | ||
| }; | ||
| const raw = await transport.request( | ||
| "tools/call", | ||
| params | ||
| ); | ||
| return parseToolResult(raw); | ||
| }, | ||
| async readServerResource(params) { | ||
| const raw = await transport.request( | ||
| READ_RESOURCE_METHOD, | ||
| params | ||
| ); | ||
| return raw; | ||
| }, | ||
| sendMessage(text, context) { | ||
| if (destroyed) return; | ||
| const textBlock = { | ||
| type: "text", | ||
| text, | ||
| ...context && { _meta: { context } } | ||
| }; | ||
| const params = { | ||
| role: "user", | ||
| content: [textBlock] | ||
| }; | ||
| transport.send(extApps.MESSAGE_METHOD, params); | ||
| }, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| resizer.destroy(); | ||
| handlers.clear(); | ||
| transport.destroy(); | ||
| } | ||
| }; | ||
| return app; | ||
| } | ||
| function injectCssVariables(vars) { | ||
| if (!vars || typeof vars !== "object") return; | ||
| for (const [k, v] of Object.entries(vars)) { | ||
| if (typeof k === "string" && typeof v === "string") { | ||
| document.documentElement.style.setProperty(k, v); | ||
| } | ||
| } | ||
| } | ||
| // src/detection.ts | ||
| var DEFAULT_THEME = { | ||
| mode: "light", | ||
| primaryColor: "#6366f1", | ||
| tokens: {} | ||
| }; | ||
| function detectHost(initResponse) { | ||
| const resp = initResponse; | ||
| const hostName = resp?.hostInfo?.name ?? "unknown"; | ||
| const protocolVersion = resp?.protocolVersion ?? "unknown"; | ||
| return { | ||
| isNimbleBrain: hostName === "nimblebrain", | ||
| serverName: hostName, | ||
| protocolVersion | ||
| }; | ||
| } | ||
| function extractTheme(ctx) { | ||
| if (!ctx) return { ...DEFAULT_THEME }; | ||
| const mode = ctx.theme === "light" || ctx.theme === "dark" ? ctx.theme : DEFAULT_THEME.mode; | ||
| const variables = ctx.styles?.variables; | ||
| const tokens = variables && typeof variables === "object" && !Array.isArray(variables) ? variables : {}; | ||
| return { mode, primaryColor: DEFAULT_THEME.primaryColor, tokens }; | ||
| } | ||
| // src/keyboard.ts | ||
| var KeyboardForwarder = class { | ||
| listener; | ||
| destroyed = false; | ||
| constructor(transport, customKeys) { | ||
| const config = customKeys ?? null; | ||
| this.listener = (event) => { | ||
| if (this.destroyed) return; | ||
| if (this.shouldForward(event, config)) { | ||
| event.preventDefault(); | ||
| transport.send("synapse/keydown", { | ||
| key: event.key, | ||
| ctrlKey: event.ctrlKey, | ||
| metaKey: event.metaKey, | ||
| shiftKey: event.shiftKey, | ||
| altKey: event.altKey | ||
| }); | ||
| } | ||
| }; | ||
| document.addEventListener("keydown", this.listener); | ||
| } | ||
| destroy() { | ||
| if (this.destroyed) return; | ||
| this.destroyed = true; | ||
| document.removeEventListener("keydown", this.listener); | ||
| } | ||
| shouldForward(event, config) { | ||
| if (config && config.length === 0) return false; | ||
| if (config) { | ||
| return config.some( | ||
| (k) => event.key.toLowerCase() === k.key.toLowerCase() && (k.ctrl === void 0 || event.ctrlKey === k.ctrl) && (k.meta === void 0 || event.metaKey === k.meta) && (k.shift === void 0 || event.shiftKey === k.shift) && (k.alt === void 0 || event.altKey === k.alt) | ||
| ); | ||
| } | ||
| if (event.key === "Escape") return true; | ||
| if (event.ctrlKey || event.metaKey) { | ||
| const key = event.key.toLowerCase(); | ||
| if (key === "c" || key === "v" || key === "x" || key === "a") return false; | ||
| return true; | ||
| } | ||
| return false; | ||
| } | ||
| }; | ||
| // src/task-handle.ts | ||
| var TOOLS_CALL_METHOD = "tools/call"; | ||
| var TASKS_GET_METHOD = "tasks/get"; | ||
| var TASKS_RESULT_METHOD = "tasks/result"; | ||
| var TASKS_CANCEL_METHOD = "tasks/cancel"; | ||
| var TASKS_STATUS_NOTIFICATION_METHOD = "notifications/tasks/status"; | ||
| function createTaskStatusRouter(transport) { | ||
| const listeners = /* @__PURE__ */ new Map(); | ||
| const unsub = transport.onMessage(TASKS_STATUS_NOTIFICATION_METHOD, (rawParams) => { | ||
| if (!rawParams) return; | ||
| const params = rawParams; | ||
| const taskId = params.taskId; | ||
| if (typeof taskId !== "string") return; | ||
| const set = listeners.get(taskId); | ||
| if (!set || set.size === 0) return; | ||
| const update = { | ||
| taskId: params.taskId, | ||
| status: params.status, | ||
| ...params.statusMessage !== void 0 && { statusMessage: params.statusMessage } | ||
| }; | ||
| for (const cb of set) cb(update); | ||
| }); | ||
| return { | ||
| subscribe(taskId, cb) { | ||
| let set = listeners.get(taskId); | ||
| if (!set) { | ||
| set = /* @__PURE__ */ new Set(); | ||
| listeners.set(taskId, set); | ||
| } | ||
| set.add(cb); | ||
| return () => { | ||
| const s = listeners.get(taskId); | ||
| if (!s) return; | ||
| s.delete(cb); | ||
| if (s.size === 0) listeners.delete(taskId); | ||
| }; | ||
| }, | ||
| dispose() { | ||
| listeners.clear(); | ||
| unsub(); | ||
| } | ||
| }; | ||
| } | ||
| async function callToolAsTask(deps, toolName, args, options) { | ||
| const hostTasks = deps.getHostTasksCapability(); | ||
| if (!hostTasks?.requests?.tools?.call) { | ||
| throw new Error( | ||
| "callToolAsTask: host did not advertise tasks.requests.tools.call in its capabilities. Per MCP 2025-11-25 \xA7, requestors MUST NOT task-augment a tools/call without matching receiver capability. Fall back to `synapse.callTool`." | ||
| ); | ||
| } | ||
| const taskParam = {}; | ||
| if (options?.ttl !== void 0) taskParam.ttl = options.ttl; | ||
| const crossServer = options?.internal ?? deps.internalApp; | ||
| const callParams = { | ||
| name: toolName, | ||
| arguments: args ?? {}, | ||
| task: taskParam, | ||
| ...crossServer ? { server: deps.appName } : {} | ||
| }; | ||
| const raw = await deps.transport.request( | ||
| TOOLS_CALL_METHOD, | ||
| callParams | ||
| ); | ||
| const createResult = raw; | ||
| const initialTask = createResult?.task; | ||
| if (!initialTask || typeof initialTask !== "object" || typeof initialTask.taskId !== "string") { | ||
| throw new Error( | ||
| "callToolAsTask: receiver returned a response without `task` per CreateTaskResult (expected shape: `{ task: { taskId, status, ... } }`). Receiver may not honor the advertised tasks capability." | ||
| ); | ||
| } | ||
| const taskId = initialTask.taskId; | ||
| const localCallbacks = /* @__PURE__ */ new Map(); | ||
| const handle = { | ||
| task: initialTask, | ||
| async result() { | ||
| const params = { taskId }; | ||
| const rawResult = await deps.transport.request( | ||
| TASKS_RESULT_METHOD, | ||
| params | ||
| ); | ||
| const typed = rawResult; | ||
| return parseToolResult(typed); | ||
| }, | ||
| async refresh() { | ||
| const params = { taskId }; | ||
| const raw2 = await deps.transport.request( | ||
| TASKS_GET_METHOD, | ||
| params | ||
| ); | ||
| return projectTask(raw2); | ||
| }, | ||
| async cancel() { | ||
| const params = { taskId }; | ||
| const raw2 = await deps.transport.request( | ||
| TASKS_CANCEL_METHOD, | ||
| params | ||
| ); | ||
| return projectTask(raw2); | ||
| }, | ||
| onStatus(cb) { | ||
| const existing = localCallbacks.get(cb); | ||
| if (existing) return existing; | ||
| const wireUnsub = deps.router.subscribe(taskId, (update) => { | ||
| const merged = { | ||
| taskId: update.taskId, | ||
| status: update.status, | ||
| ttl: initialTask.ttl, | ||
| createdAt: initialTask.createdAt, | ||
| lastUpdatedAt: (/* @__PURE__ */ new Date()).toISOString(), | ||
| ...initialTask.pollInterval !== void 0 && { | ||
| pollInterval: initialTask.pollInterval | ||
| }, | ||
| ...update.statusMessage !== void 0 && { statusMessage: update.statusMessage } | ||
| }; | ||
| cb(merged); | ||
| }); | ||
| const unsub = () => { | ||
| localCallbacks.delete(cb); | ||
| wireUnsub(); | ||
| }; | ||
| localCallbacks.set(cb, unsub); | ||
| return unsub; | ||
| } | ||
| }; | ||
| return handle; | ||
| } | ||
| function projectTask(raw) { | ||
| return { | ||
| taskId: raw.taskId, | ||
| status: raw.status, | ||
| ttl: raw.ttl, | ||
| createdAt: raw.createdAt, | ||
| lastUpdatedAt: raw.lastUpdatedAt, | ||
| ...raw.pollInterval !== void 0 && { pollInterval: raw.pollInterval }, | ||
| ...raw.statusMessage !== void 0 && { statusMessage: raw.statusMessage } | ||
| }; | ||
| } | ||
| // src/core.ts | ||
| var READ_RESOURCE_METHOD2 = "resources/read"; | ||
| function createSynapse(options) { | ||
| const { name, version, internal = false, forwardKeys } = options; | ||
| const transport = new SynapseTransport(); | ||
| let hostInfo = null; | ||
| let currentHostContext = {}; | ||
| let hostTasksCapability = null; | ||
| let destroyed = false; | ||
| const taskStatusRouter = createTaskStatusRouter(transport); | ||
| let stateTimer = null; | ||
| let keyboard = null; | ||
| const appCapabilities = { | ||
| tasks: { | ||
| cancel: {}, | ||
| requests: { tools: { call: {} } } | ||
| } | ||
| }; | ||
| const initParams = { | ||
| protocolVersion: extApps.LATEST_PROTOCOL_VERSION, | ||
| appInfo: { name, version }, | ||
| appCapabilities | ||
| }; | ||
| const ready = transport.request(extApps.INITIALIZE_METHOD, initParams).then((result) => { | ||
| hostInfo = detectHost(result); | ||
| currentHostContext = result?.hostContext ?? {}; | ||
| const initResult = result; | ||
| const rawTasks = initResult?.hostCapabilities?.tasks; | ||
| hostTasksCapability = rawTasks && typeof rawTasks === "object" && !Array.isArray(rawTasks) ? rawTasks : void 0; | ||
| injectCssVariables2(extractTheme(currentHostContext).tokens); | ||
| for (const cb of hostContextCallbacks) cb(currentHostContext); | ||
| transport.send(extApps.INITIALIZED_METHOD, {}); | ||
| keyboard = new KeyboardForwarder(transport, forwardKeys); | ||
| }); | ||
| const unsubHostContext = transport.onMessage(extApps.HOST_CONTEXT_CHANGED_METHOD, (params) => { | ||
| currentHostContext = params ?? {}; | ||
| injectCssVariables2(extractTheme(currentHostContext).tokens); | ||
| for (const cb of hostContextCallbacks) cb(currentHostContext); | ||
| }); | ||
| const hostContextCallbacks = /* @__PURE__ */ new Set(); | ||
| const dataCallbacks = /* @__PURE__ */ new Set(); | ||
| const actionCallbacks = /* @__PURE__ */ new Set(); | ||
| const unsubData = transport.onMessage("synapse/data-changed", (params) => { | ||
| if (!params) return; | ||
| const event = { | ||
| source: "agent", | ||
| server: params.server ?? "", | ||
| tool: params.tool ?? "" | ||
| }; | ||
| for (const cb of dataCallbacks) cb(event); | ||
| }); | ||
| const unsubAction = transport.onMessage("synapse/action", (params) => { | ||
| if (!params || typeof params.type !== "string") return; | ||
| const action = { | ||
| type: params.type, | ||
| payload: params.payload ?? {}, | ||
| requiresConfirmation: params.requiresConfirmation === true, | ||
| label: typeof params.label === "string" ? params.label : void 0 | ||
| }; | ||
| for (const cb of actionCallbacks) cb(action); | ||
| }); | ||
| const isNB = () => hostInfo?.isNimbleBrain === true; | ||
| const synapse = { | ||
| get ready() { | ||
| return ready; | ||
| }, | ||
| get isNimbleBrainHost() { | ||
| return isNB(); | ||
| }, | ||
| get destroyed() { | ||
| return destroyed; | ||
| }, | ||
| async callTool(toolName, args) { | ||
| const params = { | ||
| name: toolName, | ||
| arguments: args ?? {} | ||
| }; | ||
| if (internal) { | ||
| params.server = name; | ||
| } | ||
| const raw = await transport.request("tools/call", params); | ||
| return parseToolResult(raw); | ||
| }, | ||
| async callToolAsTask(toolName, args, options2) { | ||
| return callToolAsTask( | ||
| { | ||
| transport, | ||
| router: taskStatusRouter, | ||
| getHostTasksCapability: () => hostTasksCapability, | ||
| appName: name, | ||
| internalApp: internal | ||
| }, | ||
| toolName, | ||
| args, | ||
| options2 | ||
| ); | ||
| }, | ||
| async readResource(uri) { | ||
| const params = { uri }; | ||
| const raw = await transport.request( | ||
| READ_RESOURCE_METHOD2, | ||
| params | ||
| ); | ||
| return raw; | ||
| }, | ||
| onDataChanged(callback) { | ||
| dataCallbacks.add(callback); | ||
| return () => { | ||
| dataCallbacks.delete(callback); | ||
| }; | ||
| }, | ||
| onAction(callback) { | ||
| actionCallbacks.add(callback); | ||
| return () => { | ||
| actionCallbacks.delete(callback); | ||
| }; | ||
| }, | ||
| getHostContext() { | ||
| return currentHostContext; | ||
| }, | ||
| onHostContextChanged(callback) { | ||
| hostContextCallbacks.add(callback); | ||
| return () => { | ||
| hostContextCallbacks.delete(callback); | ||
| }; | ||
| }, | ||
| getTheme() { | ||
| return extractTheme(currentHostContext); | ||
| }, | ||
| // Selector over `onHostContextChanged`: only fires when the *derived* | ||
| // theme actually changes, so theme subscribers don't see spurious | ||
| // updates when other host-context fields (e.g. workspace) change. | ||
| // | ||
| // Subscriber timing matters: | ||
| // - Subscribed BEFORE handshake: `prev = null` sentinel. The first | ||
| // fire (the handshake dispatch) always invokes the callback, | ||
| // even if the host's theme happens to derive to the default. | ||
| // Otherwise consumers using `onThemeChanged` as their init | ||
| // signal would silently miss it. | ||
| // - Subscribed AFTER handshake: `prev` is pre-seeded with the | ||
| // current derived theme, so a workspace-only `host-context-changed` | ||
| // notification correctly filters as a no-op. | ||
| onThemeChanged(callback) { | ||
| let prev = hostInfo !== null ? extractTheme(currentHostContext) : null; | ||
| const wrapped = (ctx) => { | ||
| const next = extractTheme(ctx); | ||
| if (prev !== null && themesEqual(prev, next)) return; | ||
| prev = next; | ||
| callback(next); | ||
| }; | ||
| hostContextCallbacks.add(wrapped); | ||
| return () => { | ||
| hostContextCallbacks.delete(wrapped); | ||
| }; | ||
| }, | ||
| action(action, params) { | ||
| if (!isNB()) return; | ||
| transport.send("synapse/action", { action, ...params }); | ||
| }, | ||
| chat(message, context) { | ||
| const textBlock = { | ||
| type: "text", | ||
| text: message, | ||
| ...isNB() && context && { _meta: { context } } | ||
| }; | ||
| const params = { | ||
| role: "user", | ||
| content: [textBlock] | ||
| }; | ||
| transport.send(extApps.MESSAGE_METHOD, params); | ||
| }, | ||
| setVisibleState(state, summary) { | ||
| if (stateTimer) clearTimeout(stateTimer); | ||
| stateTimer = setTimeout(() => { | ||
| const params = { | ||
| structuredContent: state, | ||
| ...summary !== void 0 && { | ||
| content: [{ type: "text", text: summary }] | ||
| } | ||
| }; | ||
| transport.send("ui/update-model-context", params); | ||
| stateTimer = null; | ||
| }, 250); | ||
| }, | ||
| downloadFile(filename, content, mimeType) { | ||
| const resolvedMime = mimeType || (content instanceof Blob ? content.type : "") || "application/octet-stream"; | ||
| const blob = content instanceof Blob ? content : new Blob([content], { type: resolvedMime }); | ||
| transport.send("synapse/download-file", { | ||
| data: blob, | ||
| filename, | ||
| mimeType: resolvedMime | ||
| }); | ||
| }, | ||
| openLink(url) { | ||
| const params = { url }; | ||
| transport.request(extApps.OPEN_LINK_METHOD, params).catch(() => { | ||
| window.open(url, "_blank", "noopener"); | ||
| }); | ||
| }, | ||
| async pickFile(options2) { | ||
| if (!isNB()) { | ||
| throw new Error("pickFile is not supported in this host"); | ||
| } | ||
| const result = await transport.request("synapse/pick-file", { | ||
| accept: options2?.accept, | ||
| maxSize: options2?.maxSize ?? 26214400, | ||
| multiple: false | ||
| }); | ||
| return result ?? null; | ||
| }, | ||
| async pickFiles(options2) { | ||
| if (!isNB()) { | ||
| throw new Error("pickFiles is not supported in this host"); | ||
| } | ||
| const result = await transport.request("synapse/pick-file", { | ||
| accept: options2?.accept, | ||
| maxSize: options2?.maxSize ?? 26214400, | ||
| multiple: true | ||
| }); | ||
| if (!result) return []; | ||
| return Array.isArray(result) ? result : [result]; | ||
| }, | ||
| _onMessage(method, callback) { | ||
| return transport.onMessage(method, callback); | ||
| }, | ||
| _request(method, params) { | ||
| return transport.request(method, params); | ||
| }, | ||
| get _hostTasksCapability() { | ||
| return hostTasksCapability; | ||
| }, | ||
| destroy() { | ||
| if (destroyed) return; | ||
| destroyed = true; | ||
| if (stateTimer) clearTimeout(stateTimer); | ||
| keyboard?.destroy(); | ||
| unsubHostContext(); | ||
| unsubData(); | ||
| unsubAction(); | ||
| taskStatusRouter.dispose(); | ||
| hostContextCallbacks.clear(); | ||
| dataCallbacks.clear(); | ||
| actionCallbacks.clear(); | ||
| transport.destroy(); | ||
| } | ||
| }; | ||
| return synapse; | ||
| } | ||
| function themesEqual(a, b) { | ||
| if (a.mode !== b.mode || a.primaryColor !== b.primaryColor) return false; | ||
| const aKeys = Object.keys(a.tokens); | ||
| const bKeys = Object.keys(b.tokens); | ||
| if (aKeys.length !== bKeys.length) return false; | ||
| for (const k of aKeys) { | ||
| if (a.tokens[k] !== b.tokens[k]) return false; | ||
| } | ||
| return true; | ||
| } | ||
| function injectCssVariables2(vars) { | ||
| if (!vars || typeof vars !== "object") return; | ||
| if (typeof document === "undefined") return; | ||
| for (const [k, v] of Object.entries(vars)) { | ||
| if (typeof k === "string" && typeof v === "string") { | ||
| document.documentElement.style.setProperty(k, v); | ||
| } | ||
| } | ||
| } | ||
| exports.connect = connect; | ||
| exports.createSynapse = createSynapse; | ||
| //# sourceMappingURL=chunk-WYWKN3YG.cjs.map | ||
| //# sourceMappingURL=chunk-WYWKN3YG.cjs.map |
Sorry, the diff of this file is too big to display
| import { McpUiHostContext } from '@modelcontextprotocol/ext-apps'; | ||
| import { ReadResourceRequest, ReadResourceResult, Task } from '@modelcontextprotocol/sdk/types.js'; | ||
| /** | ||
| * Shape of the `tasks` capability advertised in `appCapabilities` on the | ||
| * iframe side (and mirrored back by the host in `hostCapabilities.tasks`). | ||
| * | ||
| * Matches the MCP 2025-11-25 tasks utility: empty objects (`{}`) are used | ||
| * as presence flags — NOT booleans — so future sub-fields can be added | ||
| * without wire-format breaks. | ||
| * | ||
| * Shape sourced from the MCP SDK's `ServerTasksCapabilitySchema` / | ||
| * `ClientCapabilities.tasks` contract. Defined locally as a plain | ||
| * interface because the SDK publishes the shape only as a Zod schema, | ||
| * not an exported TypeScript type — but the field names below are | ||
| * identical to the spec and will fail compilation against any SDK-typed | ||
| * consumer (e.g. `McpUiInitializeResult["hostCapabilities"]`) if they | ||
| * drift. | ||
| */ | ||
| interface TasksCapability { | ||
| /** Present (as `{}`) if listing tasks is supported. Deferred for MVP. */ | ||
| list?: Record<string, never>; | ||
| /** Present (as `{}`) if cancelling tasks is supported. */ | ||
| cancel?: Record<string, never>; | ||
| /** Which request types may be task-augmented. */ | ||
| requests?: { | ||
| tools?: { | ||
| /** Present (as `{}`) if `tools/call` can be task-augmented. */ | ||
| call?: Record<string, never>; | ||
| }; | ||
| }; | ||
| } | ||
| /** | ||
| * Options for task-augmenting a `tools/call` request per MCP 2025-11-25. | ||
| * | ||
| * The `task` object on `tools/call` params carries caller hints for task | ||
| * creation. The receiver MAY override (e.g. a server may enforce a lower | ||
| * TTL); clients read back the authoritative values from `CreateTaskResult.task`. | ||
| */ | ||
| interface CallToolAsTaskOptions { | ||
| /** | ||
| * Hint for how long (in milliseconds) the receiver should retain task | ||
| * results after a terminal status. Omit to let the receiver decide. | ||
| * Per spec, `null` means unlimited lifetime — represented here as the | ||
| * absence of the field (omit) since requestors rarely need to pin | ||
| * "unlimited" explicitly. | ||
| */ | ||
| ttl?: number; | ||
| /** | ||
| * Route the call through the internal-apps cross-server authz path | ||
| * (adds `params.server` set to this app's name). External apps MUST | ||
| * NOT pass this; spec doesn't touch it — it's a NimbleBrain-specific | ||
| * bridge convention mirroring `callTool`'s behavior. | ||
| */ | ||
| internal?: boolean; | ||
| } | ||
| /** | ||
| * Handle returned by `synapse.callToolAsTask`. Lifecycle mirrors the MCP | ||
| * 2025-11-25 tasks utility: the `tools/call` response is a | ||
| * `CreateTaskResult` (accessible via `task`), and the caller separately | ||
| * blocks for the terminal `CallToolResult` via `result()`. | ||
| * | ||
| * All operations route via the transport's message plumbing; no polling | ||
| * is performed here — `result()` is a blocking `tasks/result` RPC. If | ||
| * consumers want interstitial updates they can call `refresh()` or | ||
| * subscribe to `onStatus` (which is OPTIONAL per spec — hosts MAY or | ||
| * MAY NOT emit `notifications/tasks/status`). | ||
| */ | ||
| interface TaskHandle<TOutput = unknown> { | ||
| /** | ||
| * Initial task state from the `CreateTaskResult` returned by | ||
| * `tools/call`. Always populated before the handle is returned. | ||
| */ | ||
| readonly task: Task; | ||
| /** | ||
| * Send `tasks/result { taskId }` and resolve once the receiver returns | ||
| * the terminal payload. Per spec, the result shape is exactly what a | ||
| * non-task `tools/call` would return — parsed here via the shared | ||
| * `parseToolResult` so `_meta` (including | ||
| * `io.modelcontextprotocol/related-task`) propagates through. | ||
| */ | ||
| result(): Promise<ToolCallResult<TOutput>>; | ||
| /** | ||
| * Send `tasks/get { taskId }` and resolve with the current `Task`. | ||
| * Non-blocking — returns whatever status the receiver holds right now. | ||
| */ | ||
| refresh(): Promise<Task>; | ||
| /** | ||
| * Send `tasks/cancel { taskId }` and resolve with the final `Task` | ||
| * (expected `status: "cancelled"`). Cancelling an already-terminal | ||
| * task surfaces the receiver's `-32602` error. | ||
| */ | ||
| cancel(): Promise<Task>; | ||
| /** | ||
| * Subscribe to `notifications/tasks/status` events scoped to this | ||
| * handle's `taskId`. Returns an unsubscribe. Spec: status | ||
| * notifications are OPTIONAL; consumers MUST NOT depend on them for | ||
| * correctness. | ||
| */ | ||
| onStatus(cb: (task: Task) => void): () => void; | ||
| } | ||
| interface SynapseOptions { | ||
| /** App name — must match the bundle name registered with the host */ | ||
| name: string; | ||
| /** Semver version string */ | ||
| version: string; | ||
| /** | ||
| * Mark as internal NimbleBrain app. Enables cross-server tool calls. | ||
| * External apps MUST NOT set this. | ||
| */ | ||
| internal?: boolean; | ||
| /** Key combos to forward from iframe to host. Default: all Ctrl/Cmd combos + Escape. */ | ||
| forwardKeys?: KeyForwardConfig[]; | ||
| } | ||
| interface SynapseTheme { | ||
| mode: "light" | "dark"; | ||
| primaryColor: string; | ||
| tokens: Record<string, string>; | ||
| } | ||
| interface DataChangedEvent { | ||
| source: "agent"; | ||
| server: string; | ||
| tool: string; | ||
| } | ||
| /** | ||
| * Built-in action types that Synapse handles natively. | ||
| * | ||
| * - `navigate` — select/focus a resource in the UI (e.g., a board, document, record) | ||
| * - `notify` — display a transient message (toast/banner) | ||
| * - `refresh` — force a full data refresh (heavier than datachanged) | ||
| * - `confirm` — request user confirmation before the agent proceeds | ||
| * | ||
| * Apps may also receive custom string types for domain-specific actions. | ||
| */ | ||
| type BuiltinActionType = "navigate" | "notify" | "refresh" | "confirm"; | ||
| /** | ||
| * A typed, declarative action sent from the agent/server to the UI. | ||
| * | ||
| * Actions are deterministic side effects of tool execution — the tool decides | ||
| * what action to emit, not the LLM. The UI decides how to handle it. | ||
| * | ||
| * This mirrors Studio's ClientAction pattern, adapted for iframe postMessage. | ||
| */ | ||
| interface AgentAction<TPayload = Record<string, unknown>> { | ||
| /** Discriminator — a BuiltinActionType or custom string. */ | ||
| type: BuiltinActionType | (string & {}); | ||
| /** Typed payload — shape depends on `type`. */ | ||
| payload: TPayload; | ||
| /** If true, the UI should confirm with the user before executing. */ | ||
| requiresConfirmation?: boolean; | ||
| /** Human-readable label for confirmation dialogs or logs. */ | ||
| label?: string; | ||
| } | ||
| /** Payload for the built-in "navigate" action. */ | ||
| interface NavigatePayload { | ||
| /** Entity type (e.g., "board", "document", "task"). */ | ||
| entity: string; | ||
| /** Entity ID to select/focus. */ | ||
| id: string; | ||
| /** Optional sub-view or section within the entity. */ | ||
| view?: string; | ||
| } | ||
| /** Payload for the built-in "notify" action. */ | ||
| interface NotifyPayload { | ||
| message: string; | ||
| level?: "info" | "success" | "warning" | "error"; | ||
| } | ||
| interface ToolCallResult<T = unknown> { | ||
| data: T; | ||
| isError: boolean; | ||
| /** Raw MCP content blocks from the tool response. */ | ||
| content?: unknown[]; | ||
| /** | ||
| * `_meta` field from the underlying `CallToolResult`, passed through | ||
| * unchanged. Notably carries `io.modelcontextprotocol/related-task` | ||
| * (`{ taskId }`) on task-augmented results per MCP 2025-11-25. | ||
| * | ||
| * Key-preserving: any `_meta` entry the host/server attaches propagates | ||
| * without explicit support here. Consumers reading known keys should | ||
| * reference the canonical key names (e.g. `RELATED_TASK_META_KEY` from | ||
| * `@modelcontextprotocol/sdk/types.js`). | ||
| */ | ||
| _meta?: { | ||
| [key: string]: unknown; | ||
| }; | ||
| } | ||
| /** Result from a file picker request */ | ||
| interface FileResult { | ||
| filename: string; | ||
| mimeType: string; | ||
| size: number; | ||
| base64Data: string; | ||
| } | ||
| /** Options for requesting a file from the user */ | ||
| interface RequestFileOptions { | ||
| /** File type filter (e.g., ".csv,.json", "image/*") */ | ||
| accept?: string; | ||
| /** Max file size in bytes. Default: 25 MB */ | ||
| maxSize?: number; | ||
| /** Allow multiple file selection. Default: false */ | ||
| multiple?: boolean; | ||
| } | ||
| interface Synapse { | ||
| readonly ready: Promise<void>; | ||
| readonly isNimbleBrainHost: boolean; | ||
| callTool<TInput = Record<string, unknown>, TOutput = unknown>(name: string, args?: TInput): Promise<ToolCallResult<TOutput>>; | ||
| /** | ||
| * Task-augmented variant of `callTool` per MCP 2025-11-25. Sends | ||
| * `tools/call` with a `task` param; the receiver returns a | ||
| * `CreateTaskResult` promptly and the actual `CallToolResult` lands | ||
| * via `tasks/result`. Returns a `TaskHandle` that exposes | ||
| * `result()`/`refresh()`/`cancel()`/`onStatus()`. | ||
| * | ||
| * Throws if the host did not advertise `tasks.requests.tools.call` in | ||
| * its init-response capabilities — requestors MUST NOT task-augment | ||
| * without matching receiver capability. | ||
| */ | ||
| callToolAsTask<TInput = Record<string, unknown>, TOutput = unknown>(name: string, args?: TInput, options?: CallToolAsTaskOptions): Promise<TaskHandle<TOutput>>; | ||
| /** | ||
| * Read an MCP resource from the originating server via the host bridge | ||
| * (ext-apps `resources/read`). | ||
| * | ||
| * Use this to resolve `resource_link` content blocks returned by tools, or | ||
| * to fetch any known resource URI exposed by the MCP server. The host | ||
| * proxies the request to the server and forwards the result unchanged. | ||
| * | ||
| * @param uri The resource URI (e.g. `"videos://bunny-1mb"`). | ||
| * @returns The server's `ReadResourceResult` — `contents` is an array of | ||
| * blocks, each with a `uri`, optional `mimeType`, and either `text` or | ||
| * `blob` (base64). | ||
| */ | ||
| readResource(uri: string): Promise<ReadResourceResult>; | ||
| onDataChanged(callback: (event: DataChangedEvent) => void): () => void; | ||
| /** | ||
| * Subscribe to agent actions — typed, declarative commands from the server. | ||
| * | ||
| * Actions are deterministic side effects of tool execution. The server/tool | ||
| * decides what action to emit; the UI decides how to handle it. | ||
| * | ||
| * The callback receives an AgentAction with a `type` discriminator and typed | ||
| * `payload`. Apps should handle known types and ignore unknown ones. | ||
| */ | ||
| onAction(callback: (action: AgentAction) => void): () => void; | ||
| /** | ||
| * Read the current ext-apps host context as last received from the host. | ||
| * | ||
| * Spec-standardized fields (`theme`, `styles`, `displayMode`, `toolInfo`) | ||
| * are typed; the open `[key: string]: unknown` allows hosts to publish | ||
| * extensions (e.g. NimbleBrain populates `workspace`). Apps reading | ||
| * host-specific fields should treat them as optional and tolerate | ||
| * missing values when running on other hosts. | ||
| * | ||
| * Returns the empty object before the `ui/initialize` handshake completes. | ||
| */ | ||
| getHostContext(): McpUiHostContext; | ||
| /** | ||
| * Subscribe to host-context updates. Fires once per | ||
| * `ui/notifications/host-context-changed` notification (which carries a | ||
| * full snapshot, not a delta) and once on handshake completion. | ||
| * | ||
| * `getTheme`/`onThemeChanged` are typed selectors over this same state — | ||
| * prefer them when only theming matters, since they filter no-op fires. | ||
| */ | ||
| onHostContextChanged(callback: (ctx: McpUiHostContext) => void): () => void; | ||
| getTheme(): SynapseTheme; | ||
| onThemeChanged(callback: (theme: SynapseTheme) => void): () => void; | ||
| /** NimbleBrain-only: trigger a host-side action. No-op in other hosts. */ | ||
| action(action: string, params?: Record<string, unknown>): void; | ||
| /** | ||
| * Send a user message into the agent conversation (ext-apps `ui/message`). | ||
| * | ||
| * @param context NimbleBrain-specific metadata, included as `_meta.context` | ||
| * on the content block. Ignored by non-NimbleBrain hosts. | ||
| */ | ||
| chat(message: string, context?: { | ||
| action?: string; | ||
| entity?: string; | ||
| }): void; | ||
| /** | ||
| * Push the app's current visible state to the agent (ext-apps `ui/update-model-context`). | ||
| * | ||
| * The `summary` string is what the LLM reads as a text content block. | ||
| * The `state` object is included as `structuredContent` for tools that need IDs/values. | ||
| * Debounced at 250ms. Each call overwrites the previous context. | ||
| */ | ||
| setVisibleState(state: Record<string, unknown>, summary?: string): void; | ||
| downloadFile(filename: string, content: string | Blob, mimeType?: string): void; | ||
| openLink(url: string): void; | ||
| /** | ||
| * Request a file from the user via the host's native file picker. | ||
| * NimbleBrain-only: throws in non-NimbleBrain hosts. | ||
| * Returns null if the user cancels. | ||
| */ | ||
| pickFile(options?: RequestFileOptions): Promise<FileResult | null>; | ||
| /** | ||
| * Pick multiple files from the user. | ||
| * NimbleBrain-only: throws in non-NimbleBrain hosts. | ||
| * Returns empty array if the user cancels. | ||
| */ | ||
| pickFiles(options?: RequestFileOptions): Promise<FileResult[]>; | ||
| /** @internal — used by createStore for synapse/state-loaded */ | ||
| _onMessage(method: string, callback: (params: Record<string, unknown> | undefined) => void): () => void; | ||
| /** @internal — used by createStore for synapse/persist-state */ | ||
| _request(method: string, params?: Record<string, unknown>): Promise<unknown>; | ||
| /** | ||
| * @internal — host's declared `tasks` capability from the `ui/initialize` | ||
| * response, or `undefined` if absent. Read by the task-augmented tool call | ||
| * path (future `callToolAsTask`) to decide whether task augmentation is | ||
| * negotiated. `null` before the handshake completes. | ||
| * | ||
| * Requestors MUST NOT task-augment a tool call unless this is defined and | ||
| * carries `requests.tools.call` per MCP 2025-11-25. | ||
| */ | ||
| readonly _hostTasksCapability: TasksCapability | undefined | null; | ||
| /** True after destroy() has been called. */ | ||
| readonly destroyed: boolean; | ||
| destroy(): void; | ||
| } | ||
| interface VisibleState { | ||
| state: Record<string, unknown>; | ||
| summary?: string; | ||
| } | ||
| interface StateAcknowledgement { | ||
| truncated: boolean; | ||
| } | ||
| type ActionReducer<TState, TPayload = unknown> = (state: TState, payload: TPayload) => TState; | ||
| interface StoreConfig<TState> { | ||
| initialState: TState; | ||
| actions: Record<string, ActionReducer<TState, any>>; | ||
| persist?: boolean; | ||
| visibleToAgent?: boolean; | ||
| summarize?: (state: TState) => string; | ||
| version?: number; | ||
| migrations?: Array<(oldState: any) => any>; | ||
| } | ||
| type StoreDispatch<TActions extends Record<string, ActionReducer<any, any>>> = { | ||
| [K in keyof TActions]: Parameters<TActions[K]>[1] extends undefined ? () => void : (payload: Parameters<TActions[K]>[1]) => void; | ||
| }; | ||
| interface Store<TState, TActions extends Record<string, ActionReducer<TState, any>> = Record<string, ActionReducer<TState, any>>> { | ||
| getState(): TState; | ||
| subscribe(callback: (state: TState) => void): () => void; | ||
| dispatch: StoreDispatch<TActions>; | ||
| hydrate(state: TState): void; | ||
| destroy(): void; | ||
| } | ||
| interface KeyForwardConfig { | ||
| key: string; | ||
| ctrl?: boolean; | ||
| meta?: boolean; | ||
| shift?: boolean; | ||
| alt?: boolean; | ||
| } | ||
| interface ToolDefinition { | ||
| name: string; | ||
| description?: string; | ||
| inputSchema: Record<string, unknown>; | ||
| outputSchema?: Record<string, unknown>; | ||
| } | ||
| interface HostInfo { | ||
| isNimbleBrain: boolean; | ||
| serverName: string; | ||
| protocolVersion: string; | ||
| } | ||
| interface ConnectOptions { | ||
| name: string; | ||
| version: string; | ||
| autoResize?: boolean; | ||
| /** Pre-register event handlers before the handshake completes. | ||
| * These are wired before `initialized` is sent, so no messages are lost. */ | ||
| on?: Record<string, (data: any) => void>; | ||
| } | ||
| interface Theme { | ||
| mode: "light" | "dark"; | ||
| tokens: Record<string, string>; | ||
| } | ||
| interface Dimensions { | ||
| width?: number; | ||
| height?: number; | ||
| maxWidth?: number; | ||
| maxHeight?: number; | ||
| } | ||
| interface ToolResultData { | ||
| content: unknown; | ||
| structuredContent: unknown; | ||
| raw: Record<string, unknown>; | ||
| } | ||
| /** Known short event names for App.on() */ | ||
| type AppEventName = "tool-result" | "tool-input" | "tool-input-partial" | "tool-cancelled" | "theme-changed" | "teardown"; | ||
| interface App { | ||
| readonly theme: Theme; | ||
| readonly hostInfo: { | ||
| name: string; | ||
| version: string; | ||
| }; | ||
| readonly toolInfo: { | ||
| tool: Record<string, unknown>; | ||
| } | null; | ||
| readonly containerDimensions: Dimensions | null; | ||
| on(event: "tool-input", handler: (args: Record<string, unknown>) => void): () => void; | ||
| on(event: "tool-result", handler: (data: ToolResultData) => void): () => void; | ||
| on(event: "theme-changed", handler: (theme: Theme) => void): () => void; | ||
| on(event: "teardown", handler: () => void): () => void; | ||
| on(event: string, handler: (params: unknown) => void): () => void; | ||
| resize(width?: number, height?: number): void; | ||
| openLink(url: string): void; | ||
| updateModelContext(state: Record<string, unknown>, summary?: string): void; | ||
| callTool(name: string, args?: Record<string, unknown>): Promise<ToolCallResult>; | ||
| /** | ||
| * Read an MCP resource from the originating server via the host bridge | ||
| * (ext-apps `resources/read`). Named to mirror the ext-apps spec's | ||
| * `App.readServerResource`. | ||
| */ | ||
| readServerResource(params: ReadResourceRequest["params"]): Promise<ReadResourceResult>; | ||
| sendMessage(text: string, context?: { | ||
| action?: string; | ||
| entity?: string; | ||
| }): void; | ||
| destroy(): void; | ||
| } | ||
| export type { App as A, BuiltinActionType as B, ConnectOptions as C, DataChangedEvent as D, FileResult as F, HostInfo as H, KeyForwardConfig as K, NavigatePayload as N, RequestFileOptions as R, SynapseOptions as S, ToolDefinition as T, VisibleState as V, Synapse as a, ActionReducer as b, StoreConfig as c, Store as d, AgentAction as e, AppEventName as f, CallToolAsTaskOptions as g, Dimensions as h, NotifyPayload as i, StateAcknowledgement as j, StoreDispatch as k, SynapseTheme as l, TaskHandle as m, TasksCapability as n, Theme as o, ToolCallResult as p, ToolResultData as q }; |
| import { McpUiHostContext } from '@modelcontextprotocol/ext-apps'; | ||
| import { ReadResourceRequest, ReadResourceResult, Task } from '@modelcontextprotocol/sdk/types.js'; | ||
| /** | ||
| * Shape of the `tasks` capability advertised in `appCapabilities` on the | ||
| * iframe side (and mirrored back by the host in `hostCapabilities.tasks`). | ||
| * | ||
| * Matches the MCP 2025-11-25 tasks utility: empty objects (`{}`) are used | ||
| * as presence flags — NOT booleans — so future sub-fields can be added | ||
| * without wire-format breaks. | ||
| * | ||
| * Shape sourced from the MCP SDK's `ServerTasksCapabilitySchema` / | ||
| * `ClientCapabilities.tasks` contract. Defined locally as a plain | ||
| * interface because the SDK publishes the shape only as a Zod schema, | ||
| * not an exported TypeScript type — but the field names below are | ||
| * identical to the spec and will fail compilation against any SDK-typed | ||
| * consumer (e.g. `McpUiInitializeResult["hostCapabilities"]`) if they | ||
| * drift. | ||
| */ | ||
| interface TasksCapability { | ||
| /** Present (as `{}`) if listing tasks is supported. Deferred for MVP. */ | ||
| list?: Record<string, never>; | ||
| /** Present (as `{}`) if cancelling tasks is supported. */ | ||
| cancel?: Record<string, never>; | ||
| /** Which request types may be task-augmented. */ | ||
| requests?: { | ||
| tools?: { | ||
| /** Present (as `{}`) if `tools/call` can be task-augmented. */ | ||
| call?: Record<string, never>; | ||
| }; | ||
| }; | ||
| } | ||
| /** | ||
| * Options for task-augmenting a `tools/call` request per MCP 2025-11-25. | ||
| * | ||
| * The `task` object on `tools/call` params carries caller hints for task | ||
| * creation. The receiver MAY override (e.g. a server may enforce a lower | ||
| * TTL); clients read back the authoritative values from `CreateTaskResult.task`. | ||
| */ | ||
| interface CallToolAsTaskOptions { | ||
| /** | ||
| * Hint for how long (in milliseconds) the receiver should retain task | ||
| * results after a terminal status. Omit to let the receiver decide. | ||
| * Per spec, `null` means unlimited lifetime — represented here as the | ||
| * absence of the field (omit) since requestors rarely need to pin | ||
| * "unlimited" explicitly. | ||
| */ | ||
| ttl?: number; | ||
| /** | ||
| * Route the call through the internal-apps cross-server authz path | ||
| * (adds `params.server` set to this app's name). External apps MUST | ||
| * NOT pass this; spec doesn't touch it — it's a NimbleBrain-specific | ||
| * bridge convention mirroring `callTool`'s behavior. | ||
| */ | ||
| internal?: boolean; | ||
| } | ||
| /** | ||
| * Handle returned by `synapse.callToolAsTask`. Lifecycle mirrors the MCP | ||
| * 2025-11-25 tasks utility: the `tools/call` response is a | ||
| * `CreateTaskResult` (accessible via `task`), and the caller separately | ||
| * blocks for the terminal `CallToolResult` via `result()`. | ||
| * | ||
| * All operations route via the transport's message plumbing; no polling | ||
| * is performed here — `result()` is a blocking `tasks/result` RPC. If | ||
| * consumers want interstitial updates they can call `refresh()` or | ||
| * subscribe to `onStatus` (which is OPTIONAL per spec — hosts MAY or | ||
| * MAY NOT emit `notifications/tasks/status`). | ||
| */ | ||
| interface TaskHandle<TOutput = unknown> { | ||
| /** | ||
| * Initial task state from the `CreateTaskResult` returned by | ||
| * `tools/call`. Always populated before the handle is returned. | ||
| */ | ||
| readonly task: Task; | ||
| /** | ||
| * Send `tasks/result { taskId }` and resolve once the receiver returns | ||
| * the terminal payload. Per spec, the result shape is exactly what a | ||
| * non-task `tools/call` would return — parsed here via the shared | ||
| * `parseToolResult` so `_meta` (including | ||
| * `io.modelcontextprotocol/related-task`) propagates through. | ||
| */ | ||
| result(): Promise<ToolCallResult<TOutput>>; | ||
| /** | ||
| * Send `tasks/get { taskId }` and resolve with the current `Task`. | ||
| * Non-blocking — returns whatever status the receiver holds right now. | ||
| */ | ||
| refresh(): Promise<Task>; | ||
| /** | ||
| * Send `tasks/cancel { taskId }` and resolve with the final `Task` | ||
| * (expected `status: "cancelled"`). Cancelling an already-terminal | ||
| * task surfaces the receiver's `-32602` error. | ||
| */ | ||
| cancel(): Promise<Task>; | ||
| /** | ||
| * Subscribe to `notifications/tasks/status` events scoped to this | ||
| * handle's `taskId`. Returns an unsubscribe. Spec: status | ||
| * notifications are OPTIONAL; consumers MUST NOT depend on them for | ||
| * correctness. | ||
| */ | ||
| onStatus(cb: (task: Task) => void): () => void; | ||
| } | ||
| interface SynapseOptions { | ||
| /** App name — must match the bundle name registered with the host */ | ||
| name: string; | ||
| /** Semver version string */ | ||
| version: string; | ||
| /** | ||
| * Mark as internal NimbleBrain app. Enables cross-server tool calls. | ||
| * External apps MUST NOT set this. | ||
| */ | ||
| internal?: boolean; | ||
| /** Key combos to forward from iframe to host. Default: all Ctrl/Cmd combos + Escape. */ | ||
| forwardKeys?: KeyForwardConfig[]; | ||
| } | ||
| interface SynapseTheme { | ||
| mode: "light" | "dark"; | ||
| primaryColor: string; | ||
| tokens: Record<string, string>; | ||
| } | ||
| interface DataChangedEvent { | ||
| source: "agent"; | ||
| server: string; | ||
| tool: string; | ||
| } | ||
| /** | ||
| * Built-in action types that Synapse handles natively. | ||
| * | ||
| * - `navigate` — select/focus a resource in the UI (e.g., a board, document, record) | ||
| * - `notify` — display a transient message (toast/banner) | ||
| * - `refresh` — force a full data refresh (heavier than datachanged) | ||
| * - `confirm` — request user confirmation before the agent proceeds | ||
| * | ||
| * Apps may also receive custom string types for domain-specific actions. | ||
| */ | ||
| type BuiltinActionType = "navigate" | "notify" | "refresh" | "confirm"; | ||
| /** | ||
| * A typed, declarative action sent from the agent/server to the UI. | ||
| * | ||
| * Actions are deterministic side effects of tool execution — the tool decides | ||
| * what action to emit, not the LLM. The UI decides how to handle it. | ||
| * | ||
| * This mirrors Studio's ClientAction pattern, adapted for iframe postMessage. | ||
| */ | ||
| interface AgentAction<TPayload = Record<string, unknown>> { | ||
| /** Discriminator — a BuiltinActionType or custom string. */ | ||
| type: BuiltinActionType | (string & {}); | ||
| /** Typed payload — shape depends on `type`. */ | ||
| payload: TPayload; | ||
| /** If true, the UI should confirm with the user before executing. */ | ||
| requiresConfirmation?: boolean; | ||
| /** Human-readable label for confirmation dialogs or logs. */ | ||
| label?: string; | ||
| } | ||
| /** Payload for the built-in "navigate" action. */ | ||
| interface NavigatePayload { | ||
| /** Entity type (e.g., "board", "document", "task"). */ | ||
| entity: string; | ||
| /** Entity ID to select/focus. */ | ||
| id: string; | ||
| /** Optional sub-view or section within the entity. */ | ||
| view?: string; | ||
| } | ||
| /** Payload for the built-in "notify" action. */ | ||
| interface NotifyPayload { | ||
| message: string; | ||
| level?: "info" | "success" | "warning" | "error"; | ||
| } | ||
| interface ToolCallResult<T = unknown> { | ||
| data: T; | ||
| isError: boolean; | ||
| /** Raw MCP content blocks from the tool response. */ | ||
| content?: unknown[]; | ||
| /** | ||
| * `_meta` field from the underlying `CallToolResult`, passed through | ||
| * unchanged. Notably carries `io.modelcontextprotocol/related-task` | ||
| * (`{ taskId }`) on task-augmented results per MCP 2025-11-25. | ||
| * | ||
| * Key-preserving: any `_meta` entry the host/server attaches propagates | ||
| * without explicit support here. Consumers reading known keys should | ||
| * reference the canonical key names (e.g. `RELATED_TASK_META_KEY` from | ||
| * `@modelcontextprotocol/sdk/types.js`). | ||
| */ | ||
| _meta?: { | ||
| [key: string]: unknown; | ||
| }; | ||
| } | ||
| /** Result from a file picker request */ | ||
| interface FileResult { | ||
| filename: string; | ||
| mimeType: string; | ||
| size: number; | ||
| base64Data: string; | ||
| } | ||
| /** Options for requesting a file from the user */ | ||
| interface RequestFileOptions { | ||
| /** File type filter (e.g., ".csv,.json", "image/*") */ | ||
| accept?: string; | ||
| /** Max file size in bytes. Default: 25 MB */ | ||
| maxSize?: number; | ||
| /** Allow multiple file selection. Default: false */ | ||
| multiple?: boolean; | ||
| } | ||
| interface Synapse { | ||
| readonly ready: Promise<void>; | ||
| readonly isNimbleBrainHost: boolean; | ||
| callTool<TInput = Record<string, unknown>, TOutput = unknown>(name: string, args?: TInput): Promise<ToolCallResult<TOutput>>; | ||
| /** | ||
| * Task-augmented variant of `callTool` per MCP 2025-11-25. Sends | ||
| * `tools/call` with a `task` param; the receiver returns a | ||
| * `CreateTaskResult` promptly and the actual `CallToolResult` lands | ||
| * via `tasks/result`. Returns a `TaskHandle` that exposes | ||
| * `result()`/`refresh()`/`cancel()`/`onStatus()`. | ||
| * | ||
| * Throws if the host did not advertise `tasks.requests.tools.call` in | ||
| * its init-response capabilities — requestors MUST NOT task-augment | ||
| * without matching receiver capability. | ||
| */ | ||
| callToolAsTask<TInput = Record<string, unknown>, TOutput = unknown>(name: string, args?: TInput, options?: CallToolAsTaskOptions): Promise<TaskHandle<TOutput>>; | ||
| /** | ||
| * Read an MCP resource from the originating server via the host bridge | ||
| * (ext-apps `resources/read`). | ||
| * | ||
| * Use this to resolve `resource_link` content blocks returned by tools, or | ||
| * to fetch any known resource URI exposed by the MCP server. The host | ||
| * proxies the request to the server and forwards the result unchanged. | ||
| * | ||
| * @param uri The resource URI (e.g. `"videos://bunny-1mb"`). | ||
| * @returns The server's `ReadResourceResult` — `contents` is an array of | ||
| * blocks, each with a `uri`, optional `mimeType`, and either `text` or | ||
| * `blob` (base64). | ||
| */ | ||
| readResource(uri: string): Promise<ReadResourceResult>; | ||
| onDataChanged(callback: (event: DataChangedEvent) => void): () => void; | ||
| /** | ||
| * Subscribe to agent actions — typed, declarative commands from the server. | ||
| * | ||
| * Actions are deterministic side effects of tool execution. The server/tool | ||
| * decides what action to emit; the UI decides how to handle it. | ||
| * | ||
| * The callback receives an AgentAction with a `type` discriminator and typed | ||
| * `payload`. Apps should handle known types and ignore unknown ones. | ||
| */ | ||
| onAction(callback: (action: AgentAction) => void): () => void; | ||
| /** | ||
| * Read the current ext-apps host context as last received from the host. | ||
| * | ||
| * Spec-standardized fields (`theme`, `styles`, `displayMode`, `toolInfo`) | ||
| * are typed; the open `[key: string]: unknown` allows hosts to publish | ||
| * extensions (e.g. NimbleBrain populates `workspace`). Apps reading | ||
| * host-specific fields should treat them as optional and tolerate | ||
| * missing values when running on other hosts. | ||
| * | ||
| * Returns the empty object before the `ui/initialize` handshake completes. | ||
| */ | ||
| getHostContext(): McpUiHostContext; | ||
| /** | ||
| * Subscribe to host-context updates. Fires once per | ||
| * `ui/notifications/host-context-changed` notification (which carries a | ||
| * full snapshot, not a delta) and once on handshake completion. | ||
| * | ||
| * `getTheme`/`onThemeChanged` are typed selectors over this same state — | ||
| * prefer them when only theming matters, since they filter no-op fires. | ||
| */ | ||
| onHostContextChanged(callback: (ctx: McpUiHostContext) => void): () => void; | ||
| getTheme(): SynapseTheme; | ||
| onThemeChanged(callback: (theme: SynapseTheme) => void): () => void; | ||
| /** NimbleBrain-only: trigger a host-side action. No-op in other hosts. */ | ||
| action(action: string, params?: Record<string, unknown>): void; | ||
| /** | ||
| * Send a user message into the agent conversation (ext-apps `ui/message`). | ||
| * | ||
| * @param context NimbleBrain-specific metadata, included as `_meta.context` | ||
| * on the content block. Ignored by non-NimbleBrain hosts. | ||
| */ | ||
| chat(message: string, context?: { | ||
| action?: string; | ||
| entity?: string; | ||
| }): void; | ||
| /** | ||
| * Push the app's current visible state to the agent (ext-apps `ui/update-model-context`). | ||
| * | ||
| * The `summary` string is what the LLM reads as a text content block. | ||
| * The `state` object is included as `structuredContent` for tools that need IDs/values. | ||
| * Debounced at 250ms. Each call overwrites the previous context. | ||
| */ | ||
| setVisibleState(state: Record<string, unknown>, summary?: string): void; | ||
| downloadFile(filename: string, content: string | Blob, mimeType?: string): void; | ||
| openLink(url: string): void; | ||
| /** | ||
| * Request a file from the user via the host's native file picker. | ||
| * NimbleBrain-only: throws in non-NimbleBrain hosts. | ||
| * Returns null if the user cancels. | ||
| */ | ||
| pickFile(options?: RequestFileOptions): Promise<FileResult | null>; | ||
| /** | ||
| * Pick multiple files from the user. | ||
| * NimbleBrain-only: throws in non-NimbleBrain hosts. | ||
| * Returns empty array if the user cancels. | ||
| */ | ||
| pickFiles(options?: RequestFileOptions): Promise<FileResult[]>; | ||
| /** @internal — used by createStore for synapse/state-loaded */ | ||
| _onMessage(method: string, callback: (params: Record<string, unknown> | undefined) => void): () => void; | ||
| /** @internal — used by createStore for synapse/persist-state */ | ||
| _request(method: string, params?: Record<string, unknown>): Promise<unknown>; | ||
| /** | ||
| * @internal — host's declared `tasks` capability from the `ui/initialize` | ||
| * response, or `undefined` if absent. Read by the task-augmented tool call | ||
| * path (future `callToolAsTask`) to decide whether task augmentation is | ||
| * negotiated. `null` before the handshake completes. | ||
| * | ||
| * Requestors MUST NOT task-augment a tool call unless this is defined and | ||
| * carries `requests.tools.call` per MCP 2025-11-25. | ||
| */ | ||
| readonly _hostTasksCapability: TasksCapability | undefined | null; | ||
| /** True after destroy() has been called. */ | ||
| readonly destroyed: boolean; | ||
| destroy(): void; | ||
| } | ||
| interface VisibleState { | ||
| state: Record<string, unknown>; | ||
| summary?: string; | ||
| } | ||
| interface StateAcknowledgement { | ||
| truncated: boolean; | ||
| } | ||
| type ActionReducer<TState, TPayload = unknown> = (state: TState, payload: TPayload) => TState; | ||
| interface StoreConfig<TState> { | ||
| initialState: TState; | ||
| actions: Record<string, ActionReducer<TState, any>>; | ||
| persist?: boolean; | ||
| visibleToAgent?: boolean; | ||
| summarize?: (state: TState) => string; | ||
| version?: number; | ||
| migrations?: Array<(oldState: any) => any>; | ||
| } | ||
| type StoreDispatch<TActions extends Record<string, ActionReducer<any, any>>> = { | ||
| [K in keyof TActions]: Parameters<TActions[K]>[1] extends undefined ? () => void : (payload: Parameters<TActions[K]>[1]) => void; | ||
| }; | ||
| interface Store<TState, TActions extends Record<string, ActionReducer<TState, any>> = Record<string, ActionReducer<TState, any>>> { | ||
| getState(): TState; | ||
| subscribe(callback: (state: TState) => void): () => void; | ||
| dispatch: StoreDispatch<TActions>; | ||
| hydrate(state: TState): void; | ||
| destroy(): void; | ||
| } | ||
| interface KeyForwardConfig { | ||
| key: string; | ||
| ctrl?: boolean; | ||
| meta?: boolean; | ||
| shift?: boolean; | ||
| alt?: boolean; | ||
| } | ||
| interface ToolDefinition { | ||
| name: string; | ||
| description?: string; | ||
| inputSchema: Record<string, unknown>; | ||
| outputSchema?: Record<string, unknown>; | ||
| } | ||
| interface HostInfo { | ||
| isNimbleBrain: boolean; | ||
| serverName: string; | ||
| protocolVersion: string; | ||
| } | ||
| interface ConnectOptions { | ||
| name: string; | ||
| version: string; | ||
| autoResize?: boolean; | ||
| /** Pre-register event handlers before the handshake completes. | ||
| * These are wired before `initialized` is sent, so no messages are lost. */ | ||
| on?: Record<string, (data: any) => void>; | ||
| } | ||
| interface Theme { | ||
| mode: "light" | "dark"; | ||
| tokens: Record<string, string>; | ||
| } | ||
| interface Dimensions { | ||
| width?: number; | ||
| height?: number; | ||
| maxWidth?: number; | ||
| maxHeight?: number; | ||
| } | ||
| interface ToolResultData { | ||
| content: unknown; | ||
| structuredContent: unknown; | ||
| raw: Record<string, unknown>; | ||
| } | ||
| /** Known short event names for App.on() */ | ||
| type AppEventName = "tool-result" | "tool-input" | "tool-input-partial" | "tool-cancelled" | "theme-changed" | "teardown"; | ||
| interface App { | ||
| readonly theme: Theme; | ||
| readonly hostInfo: { | ||
| name: string; | ||
| version: string; | ||
| }; | ||
| readonly toolInfo: { | ||
| tool: Record<string, unknown>; | ||
| } | null; | ||
| readonly containerDimensions: Dimensions | null; | ||
| on(event: "tool-input", handler: (args: Record<string, unknown>) => void): () => void; | ||
| on(event: "tool-result", handler: (data: ToolResultData) => void): () => void; | ||
| on(event: "theme-changed", handler: (theme: Theme) => void): () => void; | ||
| on(event: "teardown", handler: () => void): () => void; | ||
| on(event: string, handler: (params: unknown) => void): () => void; | ||
| resize(width?: number, height?: number): void; | ||
| openLink(url: string): void; | ||
| updateModelContext(state: Record<string, unknown>, summary?: string): void; | ||
| callTool(name: string, args?: Record<string, unknown>): Promise<ToolCallResult>; | ||
| /** | ||
| * Read an MCP resource from the originating server via the host bridge | ||
| * (ext-apps `resources/read`). Named to mirror the ext-apps spec's | ||
| * `App.readServerResource`. | ||
| */ | ||
| readServerResource(params: ReadResourceRequest["params"]): Promise<ReadResourceResult>; | ||
| sendMessage(text: string, context?: { | ||
| action?: string; | ||
| entity?: string; | ||
| }): void; | ||
| destroy(): void; | ||
| } | ||
| export type { App as A, BuiltinActionType as B, ConnectOptions as C, DataChangedEvent as D, FileResult as F, HostInfo as H, KeyForwardConfig as K, NavigatePayload as N, RequestFileOptions as R, SynapseOptions as S, ToolDefinition as T, VisibleState as V, Synapse as a, ActionReducer as b, StoreConfig as c, Store as d, AgentAction as e, AppEventName as f, CallToolAsTaskOptions as g, Dimensions as h, NotifyPayload as i, StateAcknowledgement as j, StoreDispatch as k, SynapseTheme as l, TaskHandle as m, TasksCapability as n, Theme as o, ToolCallResult as p, ToolResultData as q }; |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
1348249
0.57%7835
0.47%