@napster-corp/webmcp-toolkit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +531 -0
  3. package/bin/webmcp-toolkit.mjs +81 -0
  4. package/dist/debug.d.ts +5 -0
  5. package/dist/debug.d.ts.map +1 -0
  6. package/dist/debug.js +26 -0
  7. package/dist/debug.js.map +1 -0
  8. package/dist/dev-panel.d.ts +22 -0
  9. package/dist/dev-panel.d.ts.map +1 -0
  10. package/dist/dev-panel.js +1046 -0
  11. package/dist/dev-panel.js.map +1 -0
  12. package/dist/index.d.ts +6 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +36 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/model-context.d.ts +13 -0
  17. package/dist/model-context.d.ts.map +1 -0
  18. package/dist/model-context.js +28 -0
  19. package/dist/model-context.js.map +1 -0
  20. package/dist/resources.d.ts +15 -0
  21. package/dist/resources.d.ts.map +1 -0
  22. package/dist/resources.js +179 -0
  23. package/dist/resources.js.map +1 -0
  24. package/dist/tiers.d.ts +31 -0
  25. package/dist/tiers.d.ts.map +1 -0
  26. package/dist/tiers.js +107 -0
  27. package/dist/tiers.js.map +1 -0
  28. package/dist/types.d.ts +145 -0
  29. package/dist/types.d.ts.map +1 -0
  30. package/dist/types.js +9 -0
  31. package/dist/types.js.map +1 -0
  32. package/hooks/post-commit +17 -0
  33. package/package.json +86 -0
  34. package/skills/add-edge-mcp-dev-panel/SKILL.md +206 -0
  35. package/skills/plan-capabilities-and-state/SKILL.md +168 -0
  36. package/skills/setup-edge-mcp/SKILL.md +546 -0
  37. package/skills/sync-webmcp-tools/SKILL.md +26 -0
  38. package/src/debug.ts +26 -0
  39. package/src/dev-panel.ts +1318 -0
  40. package/src/index.ts +66 -0
  41. package/src/model-context.ts +31 -0
  42. package/src/resources.ts +207 -0
  43. package/src/tiers.ts +132 -0
  44. package/src/types.ts +177 -0
  45. package/tools/generate-capabilities.mjs +266 -0
  46. package/tools/install-hook.mjs +81 -0
  47. package/tools/runners/anthropic.mjs +75 -0
  48. package/tools/runners/copilot.mjs +63 -0
package/src/index.ts ADDED
@@ -0,0 +1,66 @@
1
+ // Public surface of @napster-corp/webmcp-toolkit.
2
+ //
3
+ // Importing this module, in a browser, does two things as an intentional side
4
+ // effect (see package.json `sideEffects`):
5
+ // 1. Guarantees `document.modelContext` exists, via the pinned WebMCP polyfill
6
+ // (a no-op when the browser supports WebMCP natively).
7
+ // 2. Installs the MCP-shaped live-state resource extension onto it.
8
+ //
9
+ // Outside a browser (SSR, workers, edge runtimes) it does nothing and touches no
10
+ // globals. The website developer writes STANDARD WebMCP — `document.modelContext
11
+ // .registerTool(...)` — and never a Napster-specific method. Our value-adds
12
+ // (safety tiers, live state, dev tooling) live off the call site, below.
13
+
14
+ import { initializeWebMCPPolyfill } from '@mcp-b/webmcp-polyfill';
15
+ import { getModelContext, isBrowserEnvironment } from './model-context.js';
16
+ import { installResourceExtension } from './resources.js';
17
+ import { debugLog } from './debug.js';
18
+
19
+ if (isBrowserEnvironment()) {
20
+ // Guarantee the standard surface. We are the version gatekeeper: the polyfill
21
+ // is pinned exactly in package.json so upstream changes never reach customers
22
+ // untested. No-op when the browser already supports WebMCP natively.
23
+ initializeWebMCPPolyfill();
24
+ const mc = getModelContext();
25
+ if (mc) {
26
+ installResourceExtension(mc);
27
+ debugLog('document.modelContext ready (polyfill) + resource extension installed');
28
+ }
29
+ }
30
+
31
+ // ---- Standard-surface adapter (the single swap point) ----
32
+ export {
33
+ getModelContext,
34
+ getModelContextWithResources,
35
+ isBrowserEnvironment,
36
+ } from './model-context.js';
37
+
38
+ // ---- Value-add #1: safety tiers (optional wrapper over standard registerTool) ----
39
+ export { registerStatefulTool, getTier, getTierMap } from './tiers.js';
40
+
41
+ // ---- Opt-in flow logging (off by default) ----
42
+ export { setDebug } from './debug.js';
43
+
44
+ // ---- Value-add #2: live state (MCP-shaped resource extension) ----
45
+ export { installResourceExtension, registerResource } from './resources.js';
46
+
47
+ // ---- Types ----
48
+ export type {
49
+ // standard WebMCP surface
50
+ ModelContext,
51
+ ModelContextWithResources,
52
+ ToolAnnotations,
53
+ ToolContent,
54
+ ToolResult,
55
+ ToolDescriptor,
56
+ ToolInfo,
57
+ // tiers
58
+ SideEffect,
59
+ StatefulToolDescriptor,
60
+ TierMeta,
61
+ // resources
62
+ ResourceDescriptor,
63
+ ResourceInfo,
64
+ ResourceUpdate,
65
+ ResourceExtension,
66
+ } from './types.js';
@@ -0,0 +1,31 @@
1
+ // The single point where the toolkit reads the standard WebMCP surface.
2
+ //
3
+ // Everything else in the package goes through `getModelContext()` and never
4
+ // touches an MCP-B-specific symbol. That keeps the polyfill swappable: replacing
5
+ // `@mcp-b/webmcp-polyfill` with our own implementation later touches only this
6
+ // file and `index.ts` (the init call).
7
+
8
+ import type { ModelContext, ModelContextWithResources } from './types.js';
9
+
10
+ /** True only inside a browser-like environment (not SSR / workers / edge). */
11
+ export function isBrowserEnvironment(): boolean {
12
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
13
+ }
14
+
15
+ /**
16
+ * Return the standard `document.modelContext`, falling back to the deprecated
17
+ * `navigator.modelContext` alias. `document` is the canonical surface as of the
18
+ * May 2026 spec change; the navigator getter is retained only for back-compat.
19
+ * Returns `undefined` outside a browser or before the polyfill has installed.
20
+ */
21
+ export function getModelContext(): ModelContext | undefined {
22
+ if (!isBrowserEnvironment()) return undefined;
23
+ const doc = document as Document & { modelContext?: ModelContext };
24
+ const nav = navigator as Navigator & { modelContext?: ModelContext };
25
+ return doc.modelContext ?? nav.modelContext;
26
+ }
27
+
28
+ /** Same as {@link getModelContext}, typed to include the toolkit's resource extension. */
29
+ export function getModelContextWithResources(): ModelContextWithResources | undefined {
30
+ return getModelContext() as ModelContextWithResources | undefined;
31
+ }
@@ -0,0 +1,207 @@
1
+ // Live state as an MCP-shaped resource extension on `document.modelContext`.
2
+ //
3
+ // WebMCP is tools-only today, so this is entirely ours. We model it on the real
4
+ // MCP resource pattern (URI identity, list/read/subscribe, an `updated` event)
5
+ // so it reads as a natural extension and converges if/when WebMCP formalizes
6
+ // resources. The methods attach directly onto `document.modelContext` (an
7
+ // `EventTarget`) as an additive Napster surface — state lives on the standard
8
+ // object rather than a separate global.
9
+ //
10
+ // Consumed by the Napster agent over our own path; NOT interoperable with
11
+ // third-party WebMCP agents until the standard adds resources.
12
+
13
+ import { getModelContextWithResources } from './model-context.js';
14
+ import { debugLog } from './debug.js';
15
+ import type {
16
+ ModelContext,
17
+ ModelContextWithResources,
18
+ ResourceDescriptor,
19
+ ResourceInfo,
20
+ ResourceUpdate,
21
+ } from './types.js';
22
+
23
+ /** Marker so we install the extension at most once per modelContext object. */
24
+ const INSTALLED = Symbol.for('napster-webmcp-toolkit/resources-installed');
25
+
26
+ const RESOURCE_UPDATED = 'resourceupdated';
27
+ const RESOURCE_LIST_CHANGED = 'resourcelistchanged';
28
+
29
+ interface InternalResource {
30
+ uri: string;
31
+ name: string;
32
+ description?: string;
33
+ mimeType?: string;
34
+ get: () => unknown | Promise<unknown>;
35
+ }
36
+
37
+ /**
38
+ * Install the resource extension onto a `document.modelContext` object. Safe to
39
+ * call repeatedly: the second call is a no-op. Mutates `mc` in place and returns
40
+ * it typed with the resource surface.
41
+ */
42
+ export function installResourceExtension(mc: ModelContext): ModelContextWithResources {
43
+ const target = mc as ModelContextWithResources & { [INSTALLED]?: boolean };
44
+ if (target[INSTALLED]) return target;
45
+
46
+ // Per-page private registries. `producerUnsubs` holds the teardown for each
47
+ // resource's push source, so we can unwire it on re-register / unregister.
48
+ const registry = new Map<string, InternalResource>();
49
+ const producerUnsubs = new Map<string, () => void>();
50
+
51
+ function dispatchListChanged(): void {
52
+ target.dispatchEvent(new Event(RESOURCE_LIST_CHANGED));
53
+ }
54
+
55
+ // Read a resource's current value, re-reading the producer's getter. On
56
+ // failure, suppress the update and warn (the next change retries) — one
57
+ // failing resource must not break the channel.
58
+ async function emitUpdate(uri: string): Promise<void> {
59
+ const resource = registry.get(uri);
60
+ if (!resource) return;
61
+ let value: unknown;
62
+ try {
63
+ value = await resource.get();
64
+ } catch (err) {
65
+ warn(`resource '${uri}' get() threw during update; emission suppressed`, err);
66
+ return;
67
+ }
68
+ const detail: ResourceUpdate = { uri, value };
69
+ target.dispatchEvent(new CustomEvent<ResourceUpdate>(RESOURCE_UPDATED, { detail }));
70
+ debugLog(`resource "${uri}" changed →`, value);
71
+ }
72
+
73
+ function registerResource<T = unknown>(resource: ResourceDescriptor<T>): () => void {
74
+ if (!resource || typeof resource.uri !== 'string' || !resource.uri) {
75
+ throw new Error('[webmcp-toolkit] resource requires a non-empty uri');
76
+ }
77
+ if (typeof resource.name !== 'string' || !resource.name) {
78
+ throw new Error(`[webmcp-toolkit] resource '${resource.uri}' requires a non-empty name`);
79
+ }
80
+ if (typeof resource.get !== 'function') {
81
+ throw new Error(`[webmcp-toolkit] resource '${resource.uri}' requires a get() function`);
82
+ }
83
+
84
+ // Tear down any prior push subscription for this uri BEFORE installing the
85
+ // new one — re-registering must never leave a stale subscription wired up.
86
+ const priorUnsub = producerUnsubs.get(resource.uri);
87
+ if (priorUnsub) {
88
+ safeCall(priorUnsub);
89
+ producerUnsubs.delete(resource.uri);
90
+ }
91
+
92
+ const isNew = !registry.has(resource.uri);
93
+ registry.set(resource.uri, {
94
+ uri: resource.uri,
95
+ name: resource.name,
96
+ description: resource.description,
97
+ mimeType: resource.mimeType,
98
+ get: resource.get as () => unknown | Promise<unknown>,
99
+ });
100
+
101
+ if (typeof resource.subscribe === 'function') {
102
+ const unsub = resource.subscribe(() => {
103
+ void emitUpdate(resource.uri);
104
+ });
105
+ if (typeof unsub === 'function') {
106
+ producerUnsubs.set(resource.uri, unsub);
107
+ } else {
108
+ warn(
109
+ `resource '${resource.uri}' subscribe() did not return an unsubscribe function. ` +
110
+ 'The subscription will leak. Return a teardown function from subscribe, or remove ' +
111
+ 'subscribe to make the resource pull-only.',
112
+ );
113
+ }
114
+ }
115
+
116
+ if (isNew) {
117
+ dispatchListChanged();
118
+ debugLog(`registered resource "${resource.uri}" (${resource.name})`);
119
+ }
120
+
121
+ let unregistered = false;
122
+ return () => {
123
+ if (unregistered) return;
124
+ unregistered = true;
125
+ const u = producerUnsubs.get(resource.uri);
126
+ if (u) {
127
+ safeCall(u);
128
+ producerUnsubs.delete(resource.uri);
129
+ }
130
+ if (registry.delete(resource.uri)) dispatchListChanged();
131
+ };
132
+ }
133
+
134
+ function getResources(): ResourceInfo[] {
135
+ return Array.from(registry.values(), (r) => ({
136
+ uri: r.uri,
137
+ name: r.name,
138
+ description: r.description,
139
+ mimeType: r.mimeType,
140
+ }));
141
+ }
142
+
143
+ async function readResource<T = unknown>(uri: string): Promise<T | undefined> {
144
+ const resource = registry.get(uri);
145
+ if (!resource) return undefined;
146
+ try {
147
+ return (await resource.get()) as T;
148
+ } catch (err) {
149
+ warn(`readResource('${uri}') failed; returning undefined`, err);
150
+ return undefined;
151
+ }
152
+ }
153
+
154
+ function subscribeResource(
155
+ uri: string,
156
+ handler: (update: ResourceUpdate) => void,
157
+ ): () => void {
158
+ const listener = (evt: Event): void => {
159
+ const detail = (evt as CustomEvent<ResourceUpdate>).detail;
160
+ if (!detail || detail.uri !== uri) return;
161
+ try {
162
+ handler(detail);
163
+ } catch (err) {
164
+ // One bad handler must not break the dispatch for others.
165
+ warn(`subscribeResource('${uri}') handler threw`, err);
166
+ }
167
+ };
168
+ target.addEventListener(RESOURCE_UPDATED, listener);
169
+ return () => target.removeEventListener(RESOURCE_UPDATED, listener);
170
+ }
171
+
172
+ Object.defineProperties(target, {
173
+ registerResource: { value: registerResource, configurable: true, writable: true },
174
+ getResources: { value: getResources, configurable: true, writable: true },
175
+ readResource: { value: readResource, configurable: true, writable: true },
176
+ subscribeResource: { value: subscribeResource, configurable: true, writable: true },
177
+ [INSTALLED]: { value: true, configurable: true },
178
+ });
179
+
180
+ return target;
181
+ }
182
+
183
+ /**
184
+ * Producer-side convenience: register a live-state resource on
185
+ * `document.modelContext` without casting. Equivalent to
186
+ * `document.modelContext.registerResource(...)` but SSR-safe (a no-op when no
187
+ * model context is present). Returns an unregister function.
188
+ */
189
+ export function registerResource<T = unknown>(resource: ResourceDescriptor<T>): () => void {
190
+ const mc = getModelContextWithResources();
191
+ if (!mc || typeof mc.registerResource !== 'function') return () => {};
192
+ return mc.registerResource(resource);
193
+ }
194
+
195
+ function safeCall(fn: () => void): void {
196
+ try {
197
+ fn();
198
+ } catch {
199
+ /* ignore teardown errors */
200
+ }
201
+ }
202
+
203
+ function warn(message: string, err?: unknown): void {
204
+ const detail = err ? `: ${err instanceof Error ? err.message : String(err)}` : '';
205
+ // eslint-disable-next-line no-console
206
+ console.warn(`[webmcp-toolkit] ${message}${detail}`);
207
+ }
package/src/tiers.ts ADDED
@@ -0,0 +1,132 @@
1
+ // Safety tiers — Napster value-add #1, kept OFF the standard call site.
2
+ //
3
+ // The standard `annotations` object only carries `readOnlyHint` and
4
+ // `untrustedContentHint`. The richer tier (`reversible` vs `irreversible`) plus
5
+ // `idempotent` have no standard field — and the pinned polyfill drops custom
6
+ // annotation keys from `getTools()`, so they cannot ride on the tool. We store
7
+ // them in a toolkit-side registry keyed by tool name instead.
8
+ //
9
+ // Developers who want gating call `registerStatefulTool(...)`, a one-line
10
+ // superset of the standard `registerTool` that ALSO records the tier. Plain
11
+ // `document.modelContext.registerTool(...)` still works and is treated as
12
+ // `reversible` by consumers. The confirmation flow is enforced by the consumer
13
+ // (Web SDK / agent), never by the model: `irreversible` requires explicit user
14
+ // confirmation, `reversible` gets a brief announce, `read` is free.
15
+
16
+ import { getModelContext } from './model-context.js';
17
+ import { debugLog } from './debug.js';
18
+ import type {
19
+ SideEffect,
20
+ StatefulToolDescriptor,
21
+ TierMeta,
22
+ ToolAnnotations,
23
+ ToolDescriptor,
24
+ } from './types.js';
25
+
26
+ /** Tier metadata keyed by tool name. Read by the dev panel and Web SDK. */
27
+ const tierRegistry = new Map<string, TierMeta>();
28
+
29
+ function annotationsForTier(
30
+ tier: SideEffect,
31
+ untrustedContent: boolean,
32
+ override: ToolAnnotations | undefined,
33
+ ): ToolAnnotations {
34
+ return {
35
+ readOnlyHint: tier === 'read',
36
+ ...(untrustedContent ? { untrustedContentHint: true } : {}),
37
+ ...(override ?? {}),
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Register a STANDARD WebMCP tool and record its safety tier. The tool is a
43
+ * plain `document.modelContext` registration — any WebMCP agent can use it — so
44
+ * there is no lock-in; the only Napster-specific thing is that the tier is
45
+ * remembered for confirmation gating.
46
+ *
47
+ * Returns an unregister function (aborts the tool's registration signal and
48
+ * forgets its tier). In SSR / non-browser environments this is a no-op.
49
+ *
50
+ * @example
51
+ * registerStatefulTool({
52
+ * name: 'cart.checkout',
53
+ * description: 'Place the order for everything in the cart.',
54
+ * inputSchema: { type: 'object', properties: {} },
55
+ * napsterTier: 'irreversible',
56
+ * async execute() {
57
+ * await checkout();
58
+ * return { content: [{ type: 'text', text: 'Order placed.' }] };
59
+ * },
60
+ * });
61
+ */
62
+ export function registerStatefulTool(tool: StatefulToolDescriptor): () => void {
63
+ if (!tool || typeof tool.name !== 'string' || !tool.name) {
64
+ throw new Error('[webmcp-toolkit] registerStatefulTool requires a non-empty name');
65
+ }
66
+ if (typeof tool.description !== 'string' || !tool.description) {
67
+ throw new Error(
68
+ `[webmcp-toolkit] tool '${tool.name}' requires a non-empty description. ` +
69
+ 'The agent reads it to decide when to invoke the tool.',
70
+ );
71
+ }
72
+ if (typeof tool.execute !== 'function') {
73
+ throw new Error(`[webmcp-toolkit] tool '${tool.name}' requires an execute() function`);
74
+ }
75
+
76
+ const tier: SideEffect = tool.napsterTier ?? 'reversible';
77
+ const idempotent = tool.idempotent ?? false;
78
+
79
+ const mc = getModelContext();
80
+ if (!mc) {
81
+ // SSR / worker / pre-polyfill: nothing to register against. Stay silent and
82
+ // return a no-op unregister, mirroring the standard's SSR-safe behavior.
83
+ return () => {};
84
+ }
85
+
86
+ const standardTool: ToolDescriptor = {
87
+ name: tool.name,
88
+ title: tool.title,
89
+ description: tool.description,
90
+ inputSchema: tool.inputSchema,
91
+ annotations: annotationsForTier(tier, tool.untrustedContent ?? false, tool.annotations),
92
+ execute: tool.execute,
93
+ };
94
+
95
+ const controller = new AbortController();
96
+ tierRegistry.set(tool.name, {
97
+ tier,
98
+ idempotent,
99
+ requiresConfirmation: tier === 'irreversible',
100
+ });
101
+ // Forget the tier when the registration is aborted, so a removed tool's tier
102
+ // does not ghost in the registry.
103
+ controller.signal.addEventListener('abort', () => tierRegistry.delete(tool.name), {
104
+ once: true,
105
+ });
106
+
107
+ void mc.registerTool(standardTool, { signal: controller.signal });
108
+ debugLog(
109
+ `registered tool "${tool.name}" — tier=${tier}` +
110
+ `${idempotent ? ', idempotent' : ''}, readOnlyHint=${standardTool.annotations?.readOnlyHint ?? false}`,
111
+ );
112
+
113
+ let unregistered = false;
114
+ return () => {
115
+ if (unregistered) return;
116
+ unregistered = true;
117
+ controller.abort();
118
+ };
119
+ }
120
+
121
+ /** Read the safety tier recorded for a tool, or `undefined` if none. */
122
+ export function getTier(name: string): TierMeta | undefined {
123
+ return tierRegistry.get(name);
124
+ }
125
+
126
+ /**
127
+ * All recorded tiers (read-only view). Consumers should treat any tool WITHOUT
128
+ * a recorded tier as `reversible` (announce-then-run).
129
+ */
130
+ export function getTierMap(): ReadonlyMap<string, TierMeta> {
131
+ return tierRegistry;
132
+ }
package/src/types.ts ADDED
@@ -0,0 +1,177 @@
1
+ // Public types for the Napster WebMCP Toolkit.
2
+ //
3
+ // The toolkit re-bases on the WebMCP standard (`document.modelContext`). The
4
+ // website developer writes STANDARD `registerTool` calls — these types describe
5
+ // the standard surface we rely on, plus the two Napster extensions that live
6
+ // OFF the standard call site: safety tiers (`registerStatefulTool`) and live
7
+ // state (the MCP-shaped resource extension).
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Standard WebMCP surface (the subset we depend on)
11
+ // ---------------------------------------------------------------------------
12
+
13
+ /**
14
+ * Standard WebMCP tool annotations. The spec's `annotations` object carries
15
+ * exactly two hints — there is intentionally no field for richer safety tiers
16
+ * (see {@link SideEffect} / {@link StatefulToolDescriptor}).
17
+ */
18
+ export interface ToolAnnotations {
19
+ /** True if the tool only reads state and never mutates it. */
20
+ readOnlyHint?: boolean;
21
+ /** True if the tool's output may contain untrusted / third-party content. */
22
+ untrustedContentHint?: boolean;
23
+ }
24
+
25
+ /** A single content item returned from a tool's `execute` callback. */
26
+ export interface ToolContent {
27
+ type: string;
28
+ text?: string;
29
+ [key: string]: unknown;
30
+ }
31
+
32
+ /** Standard WebMCP tool result shape: `{ content: [{ type: 'text', text }] }`. */
33
+ export interface ToolResult {
34
+ content: ToolContent[];
35
+ }
36
+
37
+ /** Standard `document.modelContext.registerTool(...)` descriptor. */
38
+ export interface ToolDescriptor {
39
+ /** Domain-named identifier, e.g. 'cart.add'. */
40
+ name: string;
41
+ /** Optional human-readable label (top-level, not inside annotations). */
42
+ title?: string;
43
+ /** One-sentence description — the agent reads this to decide WHEN to call. */
44
+ description: string;
45
+ /** JSON Schema for the arguments. */
46
+ inputSchema?: Record<string, unknown>;
47
+ /** Standard hints. */
48
+ annotations?: ToolAnnotations;
49
+ /** Invokes the app's real operation and returns standard content. */
50
+ execute: (input: Record<string, unknown>) => ToolResult | Promise<ToolResult>;
51
+ }
52
+
53
+ /** Tool metadata as returned by `document.modelContext.getTools()` (read layer). */
54
+ export interface ToolInfo {
55
+ name: string;
56
+ description: string;
57
+ inputSchema?: Record<string, unknown>;
58
+ title?: string;
59
+ origin?: string;
60
+ }
61
+
62
+ /**
63
+ * The standard `document.modelContext` object (an `EventTarget`). We type only
64
+ * the members the toolkit and Web SDK touch. `getTools()` / `executeTool()` are
65
+ * not yet in the formal WebIDL but are implemented by the pinned polyfill and
66
+ * by Chrome's native implementation; `toolchange` fires (a bare `Event`, no
67
+ * detail) whenever the tool list changes.
68
+ */
69
+ export interface ModelContext extends EventTarget {
70
+ registerTool(tool: ToolDescriptor, options?: { signal?: AbortSignal }): void | Promise<void>;
71
+ getTools(): Promise<ToolInfo[]>;
72
+ executeTool(
73
+ tool: ToolInfo,
74
+ inputArgsJson: string,
75
+ options?: { signal?: AbortSignal },
76
+ ): Promise<string | null>;
77
+ }
78
+
79
+ // ---------------------------------------------------------------------------
80
+ // Napster extension #1 — safety tiers (Option A: optional wrapper)
81
+ // ---------------------------------------------------------------------------
82
+
83
+ /**
84
+ * Side-effect tier — governs how carefully a consumer commits a tool. The
85
+ * standard `annotations` object cannot carry this (the polyfill drops custom
86
+ * annotation keys from `getTools()`), so the tier is stored in a toolkit-side
87
+ * registry keyed by tool name and read back via {@link TierMeta}.
88
+ */
89
+ export type SideEffect = 'read' | 'reversible' | 'irreversible';
90
+
91
+ /**
92
+ * Optional superset of {@link ToolDescriptor} for tools that want safety
93
+ * gating. Calling `registerStatefulTool` registers a STANDARD tool (so any
94
+ * WebMCP agent can use it) and additionally records its tier. Plain
95
+ * `document.modelContext.registerTool` still works and is treated as
96
+ * `reversible` (announce-then-run) by consumers.
97
+ */
98
+ export interface StatefulToolDescriptor extends Omit<ToolDescriptor, 'annotations'> {
99
+ /** Governance tier — defaults to 'reversible' when omitted. */
100
+ napsterTier?: SideEffect;
101
+ /** True if re-running with the same args is safe to repeat. */
102
+ idempotent?: boolean;
103
+ /** True if output may contain untrusted content → sets `untrustedContentHint`. */
104
+ untrustedContent?: boolean;
105
+ /** Manual annotation override; merged last (rarely needed). */
106
+ annotations?: ToolAnnotations;
107
+ }
108
+
109
+ /** Tier metadata exposed to consumers (Web SDK / dev panel) for gating. */
110
+ export interface TierMeta {
111
+ tier: SideEffect;
112
+ idempotent: boolean;
113
+ /** True when the consumer must get explicit user confirmation (tier === 'irreversible'). */
114
+ requiresConfirmation: boolean;
115
+ }
116
+
117
+ // ---------------------------------------------------------------------------
118
+ // Napster extension #2 — live state as an MCP-shaped resource extension
119
+ // ---------------------------------------------------------------------------
120
+
121
+ /**
122
+ * A live-state resource the agent can PERCEIVE, modeled on MCP resources.
123
+ *
124
+ * Add a resource only for state that changes out-of-band — state the user edits
125
+ * by hand, or state that moves server-side. If a tool already returns the
126
+ * answer, do NOT add a resource that mirrors it. The genuine value here is
127
+ * *push* (live) state; pure pull state is better modeled as a read-only tool.
128
+ */
129
+ export interface ResourceDescriptor<T = unknown> {
130
+ /** URI identity, mirrors MCP, e.g. 'state://cart'. */
131
+ uri: string;
132
+ /** Logical name, e.g. 'cart'. Required (mirrors MCP `Resource.name`). */
133
+ name: string;
134
+ /** Optional human description. */
135
+ description?: string;
136
+ /** Optional MIME type of the read value. */
137
+ mimeType?: string;
138
+ /** Returns the current, serializable value. Cheap, side-effect-free. */
139
+ get: () => T | Promise<T>;
140
+ /** Optional push source. Fires onChange on mutation; returns an unsubscribe. */
141
+ subscribe?: (onChange: () => void) => () => void;
142
+ }
143
+
144
+ /** Resource metadata as listed by `getResources()` (mirrors `resources/list`). */
145
+ export interface ResourceInfo {
146
+ uri: string;
147
+ name: string;
148
+ description?: string;
149
+ mimeType?: string;
150
+ }
151
+
152
+ /** Payload of the `resourceupdated` event / `subscribeResource` handler. */
153
+ export interface ResourceUpdate {
154
+ uri: string;
155
+ value: unknown;
156
+ }
157
+
158
+ /**
159
+ * The additive surface installed onto `document.modelContext` by the toolkit.
160
+ * Method names mirror the MCP resource methods (`resources/list`/`read`/
161
+ * `subscribe`). Consumed by the Napster agent over our own path — not
162
+ * interoperable with third-party WebMCP agents until the standard formalizes
163
+ * resources.
164
+ */
165
+ export interface ResourceExtension {
166
+ /** Producer-side: register a live-state resource. Returns an unregister fn. */
167
+ registerResource<T = unknown>(resource: ResourceDescriptor<T>): () => void;
168
+ /** Consumer-side: list resources (mirrors `resources/list`). */
169
+ getResources(): ResourceInfo[];
170
+ /** Consumer-side: read one resource's current value (mirrors `resources/read`). */
171
+ readResource<T = unknown>(uri: string): Promise<T | undefined>;
172
+ /** Consumer-side: subscribe to one resource; returns an unsubscribe. */
173
+ subscribeResource(uri: string, handler: (update: ResourceUpdate) => void): () => void;
174
+ }
175
+
176
+ /** `document.modelContext` augmented with the toolkit's resource extension. */
177
+ export type ModelContextWithResources = ModelContext & ResourceExtension;