@agent-surface/webmcp 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Wiseair S.r.l.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # @agent-surface/webmcp
2
+
3
+ > **Experimental.** The WebMCP (`navigator.modelContext`) surface area, permission model, and lifecycle are unstable; this adapter tracks them and absorbs the drift so nothing WebMCP-shaped leaks into `@agent-surface/core`. The application model stays in agent-surface — WebMCP is strictly transport/discovery.
4
+
5
+ WebMCP transport adapter for [agent-surface](https://github.com/Wiseair-srl/agent-surface): one wire-named tool per **available** capability, re-provided on every `surface-changed`. Unavailable capabilities are not registered (WebMCP has no disabled state today — the availability reason is lost on this transport; accepted limitation). The user agent is treated as the least-trusted consumer: scope the adapter and keep two-phase confirmations.
6
+
7
+ Docs: https://agent-surface-docs.vercel.app
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pnpm add @agent-surface/core @agent-surface/webmcp
13
+ ```
14
+
15
+ ## Use
16
+
17
+ ```ts
18
+ import { createWebMcpAdapter } from "@agent-surface/webmcp";
19
+
20
+ const adapter = createWebMcpAdapter({
21
+ snapshotContext: { scope: ["devices"] }, // least-trusted peer: scope it
22
+ });
23
+ adapter.start({ registry, consumer: { id: "browser-agent", kind: "webmcp" } });
24
+ ```
25
+
26
+ If `navigator.modelContext` is absent, `start()` resolves and does nothing (feature-detect, never polyfill). Capability errors ride in tool content with `code`/`retry`/`details` preserved — never protocol-level errors.
27
+
28
+ Full specification: [docs/09](https://github.com/Wiseair-srl/agent-surface/blob/main/docs/09-adapters.md).
29
+
30
+ MIT © Wiseair S.r.l.
@@ -0,0 +1,51 @@
1
+ import { AgentSurfaceRegistry, AgentConsumer, SnapshotContext, AgentCapabilityDescriptorUnion, JsonSchema, JsonValue } from '@agent-surface/core';
2
+
3
+ interface AdapterHost {
4
+ registry: AgentSurfaceRegistry;
5
+ consumer: AgentConsumer;
6
+ /** Adapter-scoped snapshot defaults (scope, budget). */
7
+ snapshotContext?: Omit<SnapshotContext, "consumer">;
8
+ }
9
+ interface AgentSurfaceAdapter {
10
+ readonly name: string;
11
+ start(host: AdapterHost): void | Promise<void>;
12
+ stop(): void | Promise<void>;
13
+ }
14
+ interface WebMcpToolInit {
15
+ name: string;
16
+ description: string;
17
+ inputSchema: JsonSchema;
18
+ execute(input: JsonValue): Promise<WebMcpToolResult>;
19
+ }
20
+ interface WebMcpToolResult {
21
+ content: Array<{
22
+ type: "text";
23
+ text: string;
24
+ }>;
25
+ isError?: boolean;
26
+ }
27
+ interface WebMcpModelContext {
28
+ provideContext(context: {
29
+ tools: WebMcpToolInit[];
30
+ }): void;
31
+ }
32
+ interface CreateWebMcpAdapterOptions {
33
+ snapshotContext?: Omit<SnapshotContext, "consumer">;
34
+ /**
35
+ * Map/curate before exposing; return null to skip a capability, undefined
36
+ * to keep the default mapping.
37
+ */
38
+ exposeCapability?: (descriptor: AgentCapabilityDescriptorUnion) => WebMcpToolInit | null | undefined;
39
+ /** Test seam: defaults to (navigator as any).modelContext. */
40
+ modelContext?: WebMcpModelContext;
41
+ }
42
+ /**
43
+ * Maps the registry onto `navigator.modelContext`, treating WebMCP strictly
44
+ * as transport/discovery: one wire-named tool per AVAILABLE capability,
45
+ * re-provided on every surface-changed; unavailable capabilities are not
46
+ * registered (WebMCP has no disabled state today — accepted limitation);
47
+ * confirmations stay two-phase; absent modelContext ⇒ start() does nothing.
48
+ */
49
+ declare function createWebMcpAdapter(options?: CreateWebMcpAdapterOptions): AgentSurfaceAdapter;
50
+
51
+ export { type AdapterHost, type AgentSurfaceAdapter, type CreateWebMcpAdapterOptions, type WebMcpModelContext, type WebMcpToolInit, type WebMcpToolResult, createWebMcpAdapter };
package/dist/index.js ADDED
@@ -0,0 +1,108 @@
1
+ // src/core-facade.ts
2
+ import { encodeWireName, encodeWireNameForInstance } from "@agent-surface/core";
3
+ function randomInvocationId() {
4
+ return `inv_${Math.random().toString(36).slice(2, 14)}`;
5
+ }
6
+
7
+ // src/index.ts
8
+ function createWebMcpAdapter(options) {
9
+ let unsubscribe;
10
+ return {
11
+ name: "webmcp",
12
+ start(host) {
13
+ const modelContext = options?.modelContext ?? globalThis.navigator?.modelContext;
14
+ if (!modelContext) return;
15
+ const provide = () => {
16
+ const snapshot = host.registry.snapshot({
17
+ consumer: host.consumer,
18
+ ...options?.snapshotContext ?? host.snapshotContext ?? {},
19
+ includeUnavailable: false
20
+ });
21
+ const tools = [];
22
+ const toTool = (descriptor, capabilityId, registrationId, inputSchema, description) => ({
23
+ name: encodeWireName(capabilityId),
24
+ description,
25
+ inputSchema,
26
+ execute: async (input) => {
27
+ const result = await host.registry.invoke(
28
+ {
29
+ invocationId: randomInvocationId(),
30
+ capabilityId,
31
+ registrationId,
32
+ surfaceVersion: snapshot.surfaceVersion,
33
+ ...input !== void 0 && Object.keys(input).length > 0 ? { input } : {}
34
+ },
35
+ { consumer: host.consumer }
36
+ );
37
+ if (result.status === "ok") {
38
+ return {
39
+ content: [{ type: "text", text: JSON.stringify(result.output ?? null) }]
40
+ };
41
+ }
42
+ return {
43
+ content: [{ type: "text", text: JSON.stringify(result.error) }],
44
+ isError: true
45
+ };
46
+ }
47
+ });
48
+ for (const component of snapshot.components) {
49
+ for (const obs of component.observations) {
50
+ if (!obs.available) continue;
51
+ const curated = options?.exposeCapability?.(obs);
52
+ if (options?.exposeCapability && curated === null) continue;
53
+ tools.push(
54
+ curated ?? toTool(
55
+ obs,
56
+ obs.capabilityId,
57
+ component.registrationId,
58
+ { type: "object", properties: {}, additionalProperties: false },
59
+ `[view \xB7 read] ${obs.description}`
60
+ )
61
+ );
62
+ }
63
+ for (const act of component.actions) {
64
+ if (!act.available) continue;
65
+ const curated = options?.exposeCapability?.(act);
66
+ if (options?.exposeCapability && curated === null) continue;
67
+ tools.push(
68
+ curated ?? toTool(
69
+ act,
70
+ act.capabilityId,
71
+ component.registrationId,
72
+ act.inputSchema,
73
+ `[view \xB7 ${act.effect}] ${act.description}`
74
+ )
75
+ );
76
+ }
77
+ }
78
+ for (const proc of snapshot.procedures) {
79
+ if (!proc.available) continue;
80
+ const curated = options?.exposeCapability?.(proc);
81
+ if (options?.exposeCapability && curated === null) continue;
82
+ tools.push(
83
+ curated ?? toTool(
84
+ proc,
85
+ proc.procedureId,
86
+ proc.registrationId,
87
+ proc.inputSchema,
88
+ `[domain \xB7 ${proc.effect}${proc.confirmation === "required" ? " \xB7 requires confirmation" : ""}] ${proc.description}`
89
+ )
90
+ );
91
+ }
92
+ modelContext.provideContext({ tools });
93
+ };
94
+ provide();
95
+ unsubscribe = host.registry.subscribe((event) => {
96
+ if (event.type === "surface-changed") provide();
97
+ });
98
+ },
99
+ stop() {
100
+ unsubscribe?.();
101
+ unsubscribe = void 0;
102
+ }
103
+ };
104
+ }
105
+ export {
106
+ createWebMcpAdapter
107
+ };
108
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core-facade.ts","../src/index.ts"],"sourcesContent":["export { encodeWireName, encodeWireNameForInstance } from \"@agent-surface/core\";\nexport type {\n AgentCapabilityDescriptorUnion,\n AgentConsumer,\n AgentSurfaceRegistry,\n JsonSchema,\n JsonValue,\n SnapshotContext,\n} from \"@agent-surface/core\";\n\nexport function randomInvocationId(): string {\n return `inv_${Math.random().toString(36).slice(2, 14)}`;\n}\n","import {\n encodeWireName,\n randomInvocationId,\n type AgentCapabilityDescriptorUnion,\n type AgentConsumer,\n type AgentSurfaceRegistry,\n type JsonSchema,\n type JsonValue,\n type SnapshotContext,\n} from \"./core-facade.js\";\n\n/* ───────────────────────── adapter contract (docs/09) ───────────────────────── */\n\nexport interface AdapterHost {\n registry: AgentSurfaceRegistry;\n consumer: AgentConsumer; // identity this adapter acts as\n /** Adapter-scoped snapshot defaults (scope, budget). */\n snapshotContext?: Omit<SnapshotContext, \"consumer\">;\n}\n\nexport interface AgentSurfaceAdapter {\n readonly name: string;\n start(host: AdapterHost): void | Promise<void>;\n stop(): void | Promise<void>;\n}\n\n/* ───────────── assumed navigator.modelContext shape (Experimental) ─────────────\n * The WebMCP surface area is unstable (OQ-2); this module encodes the current\n * assumption and absorbs drift so nothing WebMCP-shaped leaks into core.\n */\n\nexport interface WebMcpToolInit {\n name: string;\n description: string;\n inputSchema: JsonSchema;\n execute(input: JsonValue): Promise<WebMcpToolResult>;\n}\n\nexport interface WebMcpToolResult {\n content: Array<{ type: \"text\"; text: string }>;\n isError?: boolean;\n}\n\nexport interface WebMcpModelContext {\n provideContext(context: { tools: WebMcpToolInit[] }): void;\n}\n\nexport interface CreateWebMcpAdapterOptions {\n snapshotContext?: Omit<SnapshotContext, \"consumer\">;\n /**\n * Map/curate before exposing; return null to skip a capability, undefined\n * to keep the default mapping.\n */\n exposeCapability?: (\n descriptor: AgentCapabilityDescriptorUnion,\n ) => WebMcpToolInit | null | undefined;\n /** Test seam: defaults to (navigator as any).modelContext. */\n modelContext?: WebMcpModelContext;\n}\n\n/**\n * Maps the registry onto `navigator.modelContext`, treating WebMCP strictly\n * as transport/discovery: one wire-named tool per AVAILABLE capability,\n * re-provided on every surface-changed; unavailable capabilities are not\n * registered (WebMCP has no disabled state today — accepted limitation);\n * confirmations stay two-phase; absent modelContext ⇒ start() does nothing.\n */\nexport function createWebMcpAdapter(options?: CreateWebMcpAdapterOptions): AgentSurfaceAdapter {\n let unsubscribe: (() => void) | undefined;\n\n return {\n name: \"webmcp\",\n\n start(host: AdapterHost): void {\n const modelContext =\n options?.modelContext ??\n (globalThis as { navigator?: { modelContext?: WebMcpModelContext } }).navigator\n ?.modelContext;\n if (!modelContext) return; // feature-detect, never polyfill\n\n const provide = (): void => {\n const snapshot = host.registry.snapshot({\n consumer: host.consumer,\n ...(options?.snapshotContext ?? host.snapshotContext ?? {}),\n includeUnavailable: false,\n });\n\n const tools: WebMcpToolInit[] = [];\n\n const toTool = (\n descriptor: AgentCapabilityDescriptorUnion,\n capabilityId: string,\n registrationId: string,\n inputSchema: JsonSchema,\n description: string,\n ): WebMcpToolInit => ({\n name: encodeWireName(capabilityId),\n description,\n inputSchema,\n execute: async (input: JsonValue): Promise<WebMcpToolResult> => {\n const result = await host.registry.invoke(\n {\n invocationId: randomInvocationId(),\n capabilityId,\n registrationId,\n surfaceVersion: snapshot.surfaceVersion,\n ...(input !== undefined && Object.keys(input as object).length > 0\n ? { input }\n : {}),\n },\n { consumer: host.consumer },\n );\n // Capability errors ride in tool CONTENT, never protocol errors\n // (docs/07 adapter mapping): code/retry/details preserved.\n if (result.status === \"ok\") {\n return {\n content: [{ type: \"text\", text: JSON.stringify(result.output ?? null) }],\n };\n }\n return {\n content: [{ type: \"text\", text: JSON.stringify(result.error) }],\n isError: true,\n };\n },\n });\n\n for (const component of snapshot.components) {\n for (const obs of component.observations) {\n if (!obs.available) continue;\n const curated = options?.exposeCapability?.(obs);\n if (options?.exposeCapability && curated === null) continue;\n tools.push(\n curated ??\n toTool(\n obs,\n obs.capabilityId,\n component.registrationId,\n { type: \"object\", properties: {}, additionalProperties: false },\n `[view · read] ${obs.description}`,\n ),\n );\n }\n for (const act of component.actions) {\n if (!act.available) continue;\n const curated = options?.exposeCapability?.(act);\n if (options?.exposeCapability && curated === null) continue;\n tools.push(\n curated ??\n toTool(\n act,\n act.capabilityId,\n component.registrationId,\n act.inputSchema,\n `[view · ${act.effect}] ${act.description}`,\n ),\n );\n }\n }\n for (const proc of snapshot.procedures) {\n if (!proc.available) continue;\n const curated = options?.exposeCapability?.(proc);\n if (options?.exposeCapability && curated === null) continue;\n tools.push(\n curated ??\n toTool(\n proc,\n proc.procedureId,\n proc.registrationId,\n proc.inputSchema,\n `[domain · ${proc.effect}${proc.confirmation === \"required\" ? \" · requires confirmation\" : \"\"}] ${proc.description}`,\n ),\n );\n }\n\n modelContext.provideContext({ tools });\n };\n\n provide();\n unsubscribe = host.registry.subscribe((event) => {\n if (event.type === \"surface-changed\") provide();\n });\n },\n\n stop(): void {\n unsubscribe?.();\n unsubscribe = undefined;\n },\n };\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@agent-surface/webmcp",
3
+ "version": "0.1.0",
4
+ "description": "[Experimental] WebMCP (navigator.modelContext) transport adapter for agent-surface",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "LICENSE"
19
+ ],
20
+ "dependencies": {
21
+ "@agent-surface/core": "^0.1.0"
22
+ },
23
+ "devDependencies": {
24
+ "zod": "^4.1.5",
25
+ "@agent-surface/testing": "0.1.0"
26
+ },
27
+ "author": "Paolo Barbato",
28
+ "engines": {
29
+ "node": ">=20.19.0"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/Wiseair-srl/agent-surface.git",
34
+ "directory": "packages/webmcp"
35
+ },
36
+ "homepage": "https://agent-surface-docs.vercel.app",
37
+ "bugs": {
38
+ "url": "https://github.com/Wiseair-srl/agent-surface/issues"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "keywords": [
44
+ "agent-surface",
45
+ "agent",
46
+ "ai",
47
+ "llm",
48
+ "frontend",
49
+ "capabilities",
50
+ "typescript",
51
+ "webmcp",
52
+ "mcp",
53
+ "modelcontext"
54
+ ],
55
+ "size-limit": [
56
+ {
57
+ "path": "dist/index.js",
58
+ "limit": "4 kB",
59
+ "ignore": [
60
+ "@agent-surface/core"
61
+ ]
62
+ }
63
+ ],
64
+ "scripts": {
65
+ "build": "tsup",
66
+ "typecheck": "tsc --noEmit",
67
+ "size": "size-limit"
68
+ }
69
+ }