// Public types for the Napster Edge MCP. // // The toolkit re-bases on the WebMCP standard (`document.modelContext`). The // website developer writes STANDARD `registerTool` calls — these types describe // the standard surface we rely on, plus the one Napster extension that lives OFF // the standard call site: live state (the MCP-shaped resource extension). // // Safety levels are expressed with the STANDARD annotation hints below — there // is no proprietary tier API. Map the levels you care about onto the standard // hints: read → readOnlyHint; "destructive / needs confirmation" → // destructiveHint; "safe to retry" → idempotentHint. // --------------------------------------------------------------------------- // Standard WebMCP surface (the subset we depend on) // --------------------------------------------------------------------------- /** * Standard WebMCP / MCP tool annotations (the full hint set). The consumer (Web * SDK / agent) reads these off `getTools()` to gate behavior — e.g. treat * `destructiveHint: true` as "confirm with the user before calling." */ export interface ToolAnnotations { /** True if the tool only reads state and never mutates it. */ readOnlyHint?: boolean; /** True if the tool may perform destructive (non-additive) updates. */ destructiveHint?: boolean; /** True if calling repeatedly with the same args is safe (no extra effect). */ idempotentHint?: boolean; /** True if the tool's output may contain untrusted / third-party content. */ untrustedContentHint?: boolean; /** True if the tool reaches beyond local context (network, external systems). */ openWorldHint?: boolean; } /** A single content item returned from a tool's `execute` callback. */ export interface ToolContent { type: string; text?: string; [key: string]: unknown; } /** Standard WebMCP tool result shape: `{ content: [{ type: 'text', text }] }`. */ export interface ToolResult { content: ToolContent[]; } /** Standard `document.modelContext.registerTool(...)` descriptor. */ export interface ToolDescriptor { /** Domain-named identifier, e.g. 'cart.add'. */ name: string; /** Optional human-readable label (top-level, not inside annotations). */ title?: string; /** One-sentence description — the agent reads this to decide WHEN to call. */ description: string; /** JSON Schema for the arguments. */ inputSchema?: Record; /** Standard hints. */ annotations?: ToolAnnotations; /** Invokes the app's real operation and returns standard content. */ execute: (input: Record) => ToolResult | Promise; } /** * Tool metadata as returned by `document.modelContext.getTools()` (read layer). * * NOTE the asymmetry with {@link ToolDescriptor}: you REGISTER `inputSchema` as * a JSON Schema OBJECT, but you READ it back as a JSON **string** — that is * Chromium's native `getTools()` contract (see `ModelContextToolInfo` in * `@mcp-b/webmcp-types`), and the polyfill matches it for interoperability. */ export interface ToolInfo { name: string; description: string; /** * The tool's JSON Schema, **serialized as a JSON string** (Chromium's native * contract — not an object). Parse it before use: * `const schema = JSON.parse(tool.inputSchema ?? '{"type":"object"}')`. */ inputSchema?: string; title?: string; origin?: string; /** Standard hints, surfaced by the (vendored) polyfill's getTools(). */ annotations?: ToolAnnotations; } /** * The standard `document.modelContext` object (an `EventTarget`). We type only * the members the toolkit and Web SDK touch. `getTools()` / `executeTool()` are * not yet in the formal WebIDL but are implemented by the pinned polyfill and * by Chrome's native implementation; `toolchange` fires (a bare `Event`, no * detail) whenever the tool list changes. */ export interface ModelContext extends EventTarget { registerTool(tool: ToolDescriptor, options?: { signal?: AbortSignal }): void | Promise; /** * List registered tools. NOTE: each tool's `inputSchema` comes back as a * JSON **string**, not an object — see {@link ToolInfo}. */ getTools(): Promise; /** * Execute a registered tool. * * Return contract: * - Resolves with **`JSON.stringify()`** — a JSON string of the * standard envelope `{"content":[{"type":"text","text":"..."}]}`. To render * the output, parse and unwrap: `JSON.parse(result).content[0].text` (the * `text` may itself be JSON if the tool stringified data into it). * - Resolves with `null` when the tool's `execute` returned `undefined`. * - Rejects on unknown tool name, argument-validation failure, or an aborted * `options.signal`. * * Portability note: the polyfill resolves the tool by `tool.name`, but * Chromium's native implementation requires the exact object `getTools()` * returned — always pass the `getTools()` handle, never a hand-built object. */ executeTool( tool: ToolInfo, inputArgsJson: string, options?: { signal?: AbortSignal }, ): Promise; } // --------------------------------------------------------------------------- // Napster extension — live state as an MCP-shaped resource extension // --------------------------------------------------------------------------- /** * A live-state resource the agent can PERCEIVE, modeled on MCP resources. * * Add a resource only for state that changes out-of-band — state the user edits * by hand, or state that moves server-side. If a tool already returns the * answer, do NOT add a resource that mirrors it. The genuine value here is * *push* (live) state; pure pull state is better modeled as a read-only tool. */ export interface ResourceDescriptor { /** URI identity, mirrors MCP, e.g. 'state://cart'. */ uri: string; /** Logical name, e.g. 'cart'. Required (mirrors MCP `Resource.name`). */ name: string; /** Optional human description. */ description?: string; /** Optional MIME type of the read value. */ mimeType?: string; /** Returns the current, serializable value. Cheap, side-effect-free. */ get: () => T | Promise; /** * Optional push source. Fires onChange on mutation; returns an unsubscribe. * The extension dedupes consecutive pushes whose value re-reads identical * (compared by JSON serialization), so wiring a chatty store — one that * fires several times per operation — is safe: consumers only see genuine * value changes. */ subscribe?: (onChange: () => void) => () => void; } /** Resource metadata as listed by `getResources()` (mirrors `resources/list`). */ export interface ResourceInfo { uri: string; name: string; description?: string; mimeType?: string; } /** Payload of the `resourceupdated` event / `subscribeResource` handler. */ export interface ResourceUpdate { uri: string; value: unknown; } /** * The additive surface installed onto `document.modelContext` by the toolkit. * Method names mirror the MCP resource methods (`resources/list`/`read`/ * `subscribe`). Consumed by the Napster agent over our own path — not * interoperable with third-party WebMCP agents until the standard formalizes * resources. */ export interface ResourceExtension { /** Producer-side: register a live-state resource. Returns an unregister fn. */ registerResource(resource: ResourceDescriptor): () => void; /** Consumer-side: list resources (mirrors `resources/list`). */ getResources(): ResourceInfo[]; /** Consumer-side: read one resource's current value (mirrors `resources/read`). */ readResource(uri: string): Promise; /** Consumer-side: subscribe to one resource; returns an unsubscribe. */ subscribeResource(uri: string, handler: (update: ResourceUpdate) => void): () => void; } /** `document.modelContext` augmented with the toolkit's resource extension. */ export type ModelContextWithResources = ModelContext & ResourceExtension; // --------------------------------------------------------------------------- // Ambient global — document.modelContext // --------------------------------------------------------------------------- declare global { interface Document { /** * The standard WebMCP surface. Guaranteed by importing * `@napster-corp/edge-mcp` — the import installs the polyfill and the * resource extension as a side effect (replacing any native or foreign * occupant). Typed WITH the resource extension, so both * `document.modelContext.registerTool(...)` and * `document.modelContext.registerResource(...)` type-check with no * hand-written ambient declarations in the app. */ readonly modelContext: ModelContextWithResources; } }