@guuey/mcp-apps-host 0.4.0 → 0.5.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/README.md +18 -0
- package/dist/action.d.ts +6 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js +11 -6
- package/dist/card-mount.d.ts +18 -0
- package/dist/card-mount.d.ts.map +1 -1
- package/dist/card-mount.js +25 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/react.d.ts +94 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +158 -0
- package/dist/sandbox-page.d.ts +75 -0
- package/dist/sandbox-page.d.ts.map +1 -0
- package/dist/sandbox-page.js +40 -0
- package/dist/view-host-protocol.d.ts +167 -0
- package/dist/view-host-protocol.d.ts.map +1 -0
- package/dist/view-host-protocol.js +168 -0
- package/dist/view-host.d.ts +119 -0
- package/dist/view-host.d.ts.map +1 -0
- package/dist/view-host.js +143 -0
- package/package.json +18 -1
- package/src/action.ts +11 -6
- package/src/card-mount.ts +27 -0
- package/src/index.ts +33 -0
- package/src/react.tsx +268 -0
- package/src/sandbox-page.ts +116 -0
- package/src/view-host-protocol.ts +299 -0
- package/src/view-host.ts +241 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Host half of the MCP Apps (SEP-1865) App handshake, as a PURE state
|
|
3
|
+
* machine — message in, `{state, effects}` out, zero DOM (guuey#186 Gap 1).
|
|
4
|
+
*
|
|
5
|
+
* ## Why the view hangs without this
|
|
6
|
+
*
|
|
7
|
+
* A spec-following App (ggui's iframe-runtime included) opens with
|
|
8
|
+
* `ui/initialize` posted to its parent and BLOCKS — the handshake runs
|
|
9
|
+
* before the view reads any seeded boot data, so a frame whose parent
|
|
10
|
+
* never answers stays blank forever. `toolResultViewMount` prepares the
|
|
11
|
+
* document; THIS module answers the document. The two halves together are
|
|
12
|
+
* the Host role the package is named for.
|
|
13
|
+
*
|
|
14
|
+
* ## Why a pure machine
|
|
15
|
+
*
|
|
16
|
+
* Only a real browser runs the postMessage/sandbox physics, but the
|
|
17
|
+
* publish gate (guuey-sdks mirror CI) is Node-only — the exact blind spot
|
|
18
|
+
* that let the missing-host gap ship (mount-material assertions run in
|
|
19
|
+
* Node; nothing ran the negotiation). So the protocol logic lives here,
|
|
20
|
+
* exhaustively unit-tested against scripted message sequences, and the DOM
|
|
21
|
+
* glue in `view-host.ts` stays thin enough to carry no decisions. Even the
|
|
22
|
+
* negotiation timeout is a machine INPUT ({@link viewHostElapsed}), not a
|
|
23
|
+
* timer here.
|
|
24
|
+
*
|
|
25
|
+
* ## Spec posture
|
|
26
|
+
*
|
|
27
|
+
* Types and method names come from `@modelcontextprotocol/ext-apps` — the
|
|
28
|
+
* SEP-1865 surface itself, someone else's frozen contract. Zero ggui
|
|
29
|
+
* imports, deliberately (guuey#123): this host answers ANY spec-following
|
|
30
|
+
* view; everything ggui-specific stays in `ggui-render.ts` behind ggui's
|
|
31
|
+
* own published protocol package.
|
|
32
|
+
*/
|
|
33
|
+
import { type McpUiHostCapabilities, type McpUiHostContext, type McpUiInitializeResult } from "@modelcontextprotocol/ext-apps";
|
|
34
|
+
import type { McpToolStructuredContent } from "./action.js";
|
|
35
|
+
/**
|
|
36
|
+
* The host identity in the initialize result — structurally the spec's
|
|
37
|
+
* `Implementation` (which the ext-apps root does not re-export), narrowed
|
|
38
|
+
* to the two fields a host must supply.
|
|
39
|
+
*/
|
|
40
|
+
export interface ViewHostInfo {
|
|
41
|
+
name: string;
|
|
42
|
+
version: string;
|
|
43
|
+
}
|
|
44
|
+
/** The standard MCP method a view uses to reach host-proxied tools. */
|
|
45
|
+
export declare const TOOLS_CALL_METHOD = "tools/call";
|
|
46
|
+
/** A JSON-RPC id as the wire allows it. */
|
|
47
|
+
export type ViewRequestId = number | string;
|
|
48
|
+
/** The messages this host posts INTO the view frame. */
|
|
49
|
+
export interface ViewHostOutbound {
|
|
50
|
+
jsonrpc: "2.0";
|
|
51
|
+
id?: ViewRequestId;
|
|
52
|
+
method?: string;
|
|
53
|
+
params?: {
|
|
54
|
+
[key: string]: unknown;
|
|
55
|
+
};
|
|
56
|
+
result?: {
|
|
57
|
+
[key: string]: unknown;
|
|
58
|
+
};
|
|
59
|
+
error?: {
|
|
60
|
+
code: number;
|
|
61
|
+
message: string;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Where the negotiation stands, from the host's side of the boundary.
|
|
66
|
+
*
|
|
67
|
+
* - `"negotiating"` — attached; no `ui/initialize` seen yet. A plain-HTML
|
|
68
|
+
* inline card may stay here forever, legitimately: the handshake is how
|
|
69
|
+
* a spec App boots, not an obligation on arbitrary tenant HTML.
|
|
70
|
+
* - `"connected"` — `ui/initialize` was answered. The view owns its own
|
|
71
|
+
* pixels (and its own failures) from here on.
|
|
72
|
+
* - `"no-handshake"` — the caller declared the negotiation window over
|
|
73
|
+
* ({@link viewHostElapsed}) before any `ui/initialize` arrived. What
|
|
74
|
+
* that MEANS depends on the mount channel and is the renderer's call:
|
|
75
|
+
* a `"ggui"` shell always handshakes, so this phase is a boot failure
|
|
76
|
+
* there; an `"inline"` card may simply not be an App.
|
|
77
|
+
*
|
|
78
|
+
* A renderer binds to this — the failure mode must be a labeled state,
|
|
79
|
+
* never a blank page (guuey#186 audit).
|
|
80
|
+
*/
|
|
81
|
+
export type ViewHostPhase = "negotiating" | "connected" | "no-handshake";
|
|
82
|
+
/** The machine's whole state. Immutable — every transition returns a new one. */
|
|
83
|
+
export interface ViewHostState {
|
|
84
|
+
phase: ViewHostPhase;
|
|
85
|
+
/** `ui/notifications/initialized` seen (the App's post-handshake ack). */
|
|
86
|
+
initializedSeen: boolean;
|
|
87
|
+
}
|
|
88
|
+
export declare function initialViewHostState(): ViewHostState;
|
|
89
|
+
/**
|
|
90
|
+
* What the glue must DO after a transition. Effects are data so the machine
|
|
91
|
+
* stays synchronous and Node-testable; the glue performs them.
|
|
92
|
+
*/
|
|
93
|
+
export type ViewHostEffect = {
|
|
94
|
+
kind: "respond";
|
|
95
|
+
message: ViewHostOutbound;
|
|
96
|
+
} | {
|
|
97
|
+
/**
|
|
98
|
+
* A `tools/call` the config accepted for relaying. The glue runs the
|
|
99
|
+
* (async) relay hook and posts {@link toolCallResponse} with the
|
|
100
|
+
* result. Only ever emitted when {@link ViewHostBehavior.toolRelay}
|
|
101
|
+
* is true — with no relay wired, the machine refuses the call
|
|
102
|
+
* in-band instead (an honest `method_not_supported`).
|
|
103
|
+
*/
|
|
104
|
+
kind: "relay-tool-call";
|
|
105
|
+
id: ViewRequestId;
|
|
106
|
+
name: string;
|
|
107
|
+
arguments?: McpToolStructuredContent;
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* The host identity/behavior the machine answers with. Everything here is
|
|
111
|
+
* explicit config — the machine assumes nothing about the embedder.
|
|
112
|
+
*/
|
|
113
|
+
export interface ViewHostBehavior {
|
|
114
|
+
hostInfo: ViewHostInfo;
|
|
115
|
+
/**
|
|
116
|
+
* The capabilities to advertise. Empty is a correct, honest default for
|
|
117
|
+
* views that ride their own live channel (ggui views do — they boot from
|
|
118
|
+
* their seeded envelope and talk to their pod directly, so the host's
|
|
119
|
+
* whole job is unblocking the handshake). Advertise ONLY what the
|
|
120
|
+
* embedder actually implements: a capability the host does not honor
|
|
121
|
+
* makes the view attribute later failures to the wrong layer.
|
|
122
|
+
*/
|
|
123
|
+
hostCapabilities: McpUiHostCapabilities;
|
|
124
|
+
/** The context handed to the view in the initialize result. */
|
|
125
|
+
hostContext: McpUiHostContext;
|
|
126
|
+
/** Whether a `tools/call` relay hook is wired (see `view-host.ts`). */
|
|
127
|
+
toolRelay: boolean;
|
|
128
|
+
}
|
|
129
|
+
/** The result of feeding one inbound frame (or the timeout) to the machine. */
|
|
130
|
+
export interface ViewHostTransition {
|
|
131
|
+
state: ViewHostState;
|
|
132
|
+
effects: ViewHostEffect[];
|
|
133
|
+
}
|
|
134
|
+
/** The spec-canonical answer to `ui/initialize`. Exported for the glue/tests. */
|
|
135
|
+
export declare function initializeResult(behavior: ViewHostBehavior, requestedProtocolVersion: unknown): McpUiInitializeResult;
|
|
136
|
+
/** Build the in-band response for a relayed `tools/call`'s settled result. */
|
|
137
|
+
export declare function toolCallResponse(id: ViewRequestId, result: {
|
|
138
|
+
[key: string]: unknown;
|
|
139
|
+
}): ViewHostOutbound;
|
|
140
|
+
/**
|
|
141
|
+
* The spec-mannered farewell a detaching host posts (`ui/resource-teardown`).
|
|
142
|
+
* Sent WITHOUT an id — a host that is tearing the frame down cannot await a
|
|
143
|
+
* response, and an id-less JSON-RPC message is a notification the view may
|
|
144
|
+
* use for cleanup or ignore.
|
|
145
|
+
*/
|
|
146
|
+
export declare function teardownMessage(): ViewHostOutbound;
|
|
147
|
+
/**
|
|
148
|
+
* Feed one inbound postMessage payload to the machine.
|
|
149
|
+
*
|
|
150
|
+
* The contract, exactly:
|
|
151
|
+
* - non-RPC data → ignored (not ours);
|
|
152
|
+
* - notifications (no id) → consumed silently, JSON-RPC-correctly; the
|
|
153
|
+
* `ui/notifications/initialized` ack is remembered on the state;
|
|
154
|
+
* - `ui/initialize` → answered spec-canonically; phase → `"connected"`
|
|
155
|
+
* (also from `"no-handshake"` — a late handshake still gets answered:
|
|
156
|
+
* the timeout labels a state, it does not close the door);
|
|
157
|
+
* - `tools/call` with a relay wired → `relay-tool-call` effect;
|
|
158
|
+
* - every other REQUEST → `method_not_supported`, honestly.
|
|
159
|
+
*/
|
|
160
|
+
export declare function viewHostReceive(state: ViewHostState, behavior: ViewHostBehavior, data: unknown): ViewHostTransition;
|
|
161
|
+
/**
|
|
162
|
+
* Declare the negotiation window over. Meaningful only while
|
|
163
|
+
* `"negotiating"`: a connected view stays connected, and an already-lapsed
|
|
164
|
+
* one stays lapsed. The caller owns the clock — this machine has none.
|
|
165
|
+
*/
|
|
166
|
+
export declare function viewHostElapsed(state: ViewHostState): ViewHostState;
|
|
167
|
+
//# sourceMappingURL=view-host-protocol.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"view-host-protocol.d.ts","sourceRoot":"","sources":["../src/view-host-protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,EAIL,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC3B,MAAM,gCAAgC,CAAC;AACxC,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAE5D;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAKD,uEAAuE;AACvE,eAAO,MAAM,iBAAiB,eAAe,CAAC;AAE9C,2CAA2C;AAC3C,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5C,wDAAwD;AACxD,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,KAAK,CAAC;IACf,EAAE,CAAC,EAAE,aAAa,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IACpC,MAAM,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IACpC,KAAK,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3C;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,aAAa,GAAG,aAAa,GAAG,WAAW,GAAG,cAAc,CAAC;AAEzE,iFAAiF;AACjF,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,aAAa,CAAC;IACrB,0EAA0E;IAC1E,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED,wBAAgB,oBAAoB,IAAI,aAAa,CAEpD;AAED;;;GAGG;AACH,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,gBAAgB,CAAA;CAAE,GAC9C;IACE;;;;;;OAMG;IACH,IAAI,EAAE,iBAAiB,CAAC;IACxB,EAAE,EAAE,aAAa,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,wBAAwB,CAAC;CACtC,CAAC;AAEN;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,YAAY,CAAC;IACvB;;;;;;;OAOG;IACH,gBAAgB,EAAE,qBAAqB,CAAC;IACxC,+DAA+D;IAC/D,WAAW,EAAE,gBAAgB,CAAC;IAC9B,uEAAuE;IACvE,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,+EAA+E;AAC/E,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,aAAa,CAAC;IACrB,OAAO,EAAE,cAAc,EAAE,CAAC;CAC3B;AAiCD,iFAAiF;AACjF,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,gBAAgB,EAC1B,wBAAwB,EAAE,OAAO,GAChC,qBAAqB,CAcvB;AAED,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,aAAa,EACjB,MAAM,EAAE;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,GACjC,gBAAgB,CAElB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,IAAI,gBAAgB,CAElD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,aAAa,EACpB,QAAQ,EAAE,gBAAgB,EAC1B,IAAI,EAAE,OAAO,GACZ,kBAAkB,CAuDpB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,aAAa,GAAG,aAAa,CAEnE"}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Host half of the MCP Apps (SEP-1865) App handshake, as a PURE state
|
|
3
|
+
* machine — message in, `{state, effects}` out, zero DOM (guuey#186 Gap 1).
|
|
4
|
+
*
|
|
5
|
+
* ## Why the view hangs without this
|
|
6
|
+
*
|
|
7
|
+
* A spec-following App (ggui's iframe-runtime included) opens with
|
|
8
|
+
* `ui/initialize` posted to its parent and BLOCKS — the handshake runs
|
|
9
|
+
* before the view reads any seeded boot data, so a frame whose parent
|
|
10
|
+
* never answers stays blank forever. `toolResultViewMount` prepares the
|
|
11
|
+
* document; THIS module answers the document. The two halves together are
|
|
12
|
+
* the Host role the package is named for.
|
|
13
|
+
*
|
|
14
|
+
* ## Why a pure machine
|
|
15
|
+
*
|
|
16
|
+
* Only a real browser runs the postMessage/sandbox physics, but the
|
|
17
|
+
* publish gate (guuey-sdks mirror CI) is Node-only — the exact blind spot
|
|
18
|
+
* that let the missing-host gap ship (mount-material assertions run in
|
|
19
|
+
* Node; nothing ran the negotiation). So the protocol logic lives here,
|
|
20
|
+
* exhaustively unit-tested against scripted message sequences, and the DOM
|
|
21
|
+
* glue in `view-host.ts` stays thin enough to carry no decisions. Even the
|
|
22
|
+
* negotiation timeout is a machine INPUT ({@link viewHostElapsed}), not a
|
|
23
|
+
* timer here.
|
|
24
|
+
*
|
|
25
|
+
* ## Spec posture
|
|
26
|
+
*
|
|
27
|
+
* Types and method names come from `@modelcontextprotocol/ext-apps` — the
|
|
28
|
+
* SEP-1865 surface itself, someone else's frozen contract. Zero ggui
|
|
29
|
+
* imports, deliberately (guuey#123): this host answers ANY spec-following
|
|
30
|
+
* view; everything ggui-specific stays in `ggui-render.ts` behind ggui's
|
|
31
|
+
* own published protocol package.
|
|
32
|
+
*/
|
|
33
|
+
import { INITIALIZE_METHOD, LATEST_PROTOCOL_VERSION, RESOURCE_TEARDOWN_METHOD, } from "@modelcontextprotocol/ext-apps";
|
|
34
|
+
/** JSON-RPC `method not found` — the spec's code, not an invented one. */
|
|
35
|
+
const METHOD_NOT_SUPPORTED = -32601;
|
|
36
|
+
/** The standard MCP method a view uses to reach host-proxied tools. */
|
|
37
|
+
export const TOOLS_CALL_METHOD = "tools/call";
|
|
38
|
+
export function initialViewHostState() {
|
|
39
|
+
return { phase: "negotiating", initializedSeen: false };
|
|
40
|
+
}
|
|
41
|
+
/** Plain-object narrowing, same idiom as `action.ts`'s `isJsonObjectLike`. */
|
|
42
|
+
function isPlainObject(value) {
|
|
43
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Narrow untrusted postMessage data to a JSON-RPC envelope this host could
|
|
47
|
+
* answer. Anything else — other windows' chatter, the view's own non-RPC
|
|
48
|
+
* messages — is silently not ours (`undefined`), NOT an error: a shared
|
|
49
|
+
* `message` listener hears the whole page.
|
|
50
|
+
*/
|
|
51
|
+
function asInboundEnvelope(data) {
|
|
52
|
+
if (!isPlainObject(data))
|
|
53
|
+
return undefined;
|
|
54
|
+
if (data["jsonrpc"] !== "2.0")
|
|
55
|
+
return undefined;
|
|
56
|
+
const method = data["method"];
|
|
57
|
+
if (typeof method !== "string")
|
|
58
|
+
return undefined;
|
|
59
|
+
const id = data["id"];
|
|
60
|
+
const params = data["params"];
|
|
61
|
+
return {
|
|
62
|
+
...(typeof id === "number" || typeof id === "string" ? { id } : {}),
|
|
63
|
+
method,
|
|
64
|
+
...(isPlainObject(params) ? { params } : {}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** The spec-canonical answer to `ui/initialize`. Exported for the glue/tests. */
|
|
68
|
+
export function initializeResult(behavior, requestedProtocolVersion) {
|
|
69
|
+
// Echo the version the view asked for when it names one — the view is
|
|
70
|
+
// the side with a fixed runtime; the host has no version-specific
|
|
71
|
+
// behavior to defend. Absent/malformed, answer with the spec's latest.
|
|
72
|
+
const protocolVersion = typeof requestedProtocolVersion === "string" && requestedProtocolVersion.length > 0
|
|
73
|
+
? requestedProtocolVersion
|
|
74
|
+
: LATEST_PROTOCOL_VERSION;
|
|
75
|
+
return {
|
|
76
|
+
protocolVersion,
|
|
77
|
+
hostInfo: behavior.hostInfo,
|
|
78
|
+
hostCapabilities: behavior.hostCapabilities,
|
|
79
|
+
hostContext: behavior.hostContext,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/** Build the in-band response for a relayed `tools/call`'s settled result. */
|
|
83
|
+
export function toolCallResponse(id, result) {
|
|
84
|
+
return { jsonrpc: "2.0", id, result };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The spec-mannered farewell a detaching host posts (`ui/resource-teardown`).
|
|
88
|
+
* Sent WITHOUT an id — a host that is tearing the frame down cannot await a
|
|
89
|
+
* response, and an id-less JSON-RPC message is a notification the view may
|
|
90
|
+
* use for cleanup or ignore.
|
|
91
|
+
*/
|
|
92
|
+
export function teardownMessage() {
|
|
93
|
+
return { jsonrpc: "2.0", method: RESOURCE_TEARDOWN_METHOD, params: {} };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Feed one inbound postMessage payload to the machine.
|
|
97
|
+
*
|
|
98
|
+
* The contract, exactly:
|
|
99
|
+
* - non-RPC data → ignored (not ours);
|
|
100
|
+
* - notifications (no id) → consumed silently, JSON-RPC-correctly; the
|
|
101
|
+
* `ui/notifications/initialized` ack is remembered on the state;
|
|
102
|
+
* - `ui/initialize` → answered spec-canonically; phase → `"connected"`
|
|
103
|
+
* (also from `"no-handshake"` — a late handshake still gets answered:
|
|
104
|
+
* the timeout labels a state, it does not close the door);
|
|
105
|
+
* - `tools/call` with a relay wired → `relay-tool-call` effect;
|
|
106
|
+
* - every other REQUEST → `method_not_supported`, honestly.
|
|
107
|
+
*/
|
|
108
|
+
export function viewHostReceive(state, behavior, data) {
|
|
109
|
+
const req = asInboundEnvelope(data);
|
|
110
|
+
if (req === undefined)
|
|
111
|
+
return { state, effects: [] };
|
|
112
|
+
if (req.id === undefined) {
|
|
113
|
+
// A notification. Track the one the handshake defines; consume the rest.
|
|
114
|
+
if (req.method === "ui/notifications/initialized" && !state.initializedSeen) {
|
|
115
|
+
return { state: { ...state, initializedSeen: true }, effects: [] };
|
|
116
|
+
}
|
|
117
|
+
return { state, effects: [] };
|
|
118
|
+
}
|
|
119
|
+
if (req.method === INITIALIZE_METHOD) {
|
|
120
|
+
const result = initializeResult(behavior, req.params?.["protocolVersion"]);
|
|
121
|
+
return {
|
|
122
|
+
state: { ...state, phase: "connected" },
|
|
123
|
+
effects: [{ kind: "respond", message: { jsonrpc: "2.0", id: req.id, result } }],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (req.method === TOOLS_CALL_METHOD && behavior.toolRelay) {
|
|
127
|
+
const name = req.params?.["name"];
|
|
128
|
+
if (typeof name === "string") {
|
|
129
|
+
const args = req.params?.["arguments"];
|
|
130
|
+
return {
|
|
131
|
+
state,
|
|
132
|
+
effects: [
|
|
133
|
+
{
|
|
134
|
+
kind: "relay-tool-call",
|
|
135
|
+
id: req.id,
|
|
136
|
+
name,
|
|
137
|
+
...(isPlainObject(args) ? { arguments: args } : {}),
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
// fall through: a nameless tools/call is not a call we can relay.
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
state,
|
|
146
|
+
effects: [
|
|
147
|
+
{
|
|
148
|
+
kind: "respond",
|
|
149
|
+
message: {
|
|
150
|
+
jsonrpc: "2.0",
|
|
151
|
+
id: req.id,
|
|
152
|
+
error: {
|
|
153
|
+
code: METHOD_NOT_SUPPORTED,
|
|
154
|
+
message: `method_not_supported: ${req.method} — this host answers ${INITIALIZE_METHOD}${behavior.toolRelay ? ` and ${TOOLS_CALL_METHOD}` : ""} only`,
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Declare the negotiation window over. Meaningful only while
|
|
163
|
+
* `"negotiating"`: a connected view stays connected, and an already-lapsed
|
|
164
|
+
* one stays lapsed. The caller owns the clock — this machine has none.
|
|
165
|
+
*/
|
|
166
|
+
export function viewHostElapsed(state) {
|
|
167
|
+
return state.phase === "negotiating" ? { ...state, phase: "no-handshake" } : state;
|
|
168
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `attachViewHost` — the DOM glue around `view-host-protocol.ts`'s pure
|
|
3
|
+
* machine (guuey#186 Gap 1). Framework-agnostic: any embedder with an
|
|
4
|
+
* iframe can play the MCP Apps Host role with one call; `react.tsx` is one
|
|
5
|
+
* convenience composition of exactly this, and a future full transcript
|
|
6
|
+
* renderer composes the same primitive differently.
|
|
7
|
+
*
|
|
8
|
+
* This module is glue by DESIGN — every decision (what to answer, what to
|
|
9
|
+
* refuse, when the negotiation window lapses) lives in the machine, which
|
|
10
|
+
* the Node-only publish gate can test. What genuinely needs a browser is
|
|
11
|
+
* this file's five moves: listen, identity-filter, post, time, detach —
|
|
12
|
+
* covered by the monorepo's Playwright leg (`e2e/`), which the guuey-sdks
|
|
13
|
+
* mirror deliberately does not carry.
|
|
14
|
+
*
|
|
15
|
+
* ## The identity filter (security invariant)
|
|
16
|
+
*
|
|
17
|
+
* Messages are matched by `event.source === frame.contentWindow`, NEVER by
|
|
18
|
+
* `event.origin`: a view frame runs `sandbox="allow-scripts"` WITHOUT
|
|
19
|
+
* `allow-same-origin`, so its origin is opaque — every message it posts
|
|
20
|
+
* carries `"null"`, a value every other sandboxed frame on the page
|
|
21
|
+
* shares, identifying nobody. The window handle is the only identity that
|
|
22
|
+
* names the frame; a frame with no `contentWindow` matches nothing rather
|
|
23
|
+
* than everything. Responses target `'*'` for the same reason: an opaque
|
|
24
|
+
* origin is not addressable by name, and the handshake payload carries no
|
|
25
|
+
* secrets — it is the result the spec defines for any host.
|
|
26
|
+
*
|
|
27
|
+
* Seeded from ggui's console `surface-host.ts` (donated, guuey#186 audit);
|
|
28
|
+
* re-derived here against the pure machine + our own tests.
|
|
29
|
+
*/
|
|
30
|
+
import { type ViewHostPhase } from "./view-host-protocol.js";
|
|
31
|
+
import { type McpToolCallResult, type UiActionRequest } from "./action.js";
|
|
32
|
+
import type { McpUiResourcePayload } from "./block-ui.js";
|
|
33
|
+
import type { McpUiHostCapabilities, McpUiHostContext } from "@modelcontextprotocol/ext-apps";
|
|
34
|
+
import type { ViewHostInfo } from "./view-host-protocol.js";
|
|
35
|
+
/**
|
|
36
|
+
* The slice of an `HTMLIFrameElement` this host actually touches — a
|
|
37
|
+
* structural type so Node tests (and non-DOM hosts) can hand in a fake
|
|
38
|
+
* without a single cast, same injection idiom as the package's readers and
|
|
39
|
+
* relays. A real iframe element satisfies it as-is.
|
|
40
|
+
*/
|
|
41
|
+
export interface ViewFrameLike {
|
|
42
|
+
readonly contentWindow: {
|
|
43
|
+
postMessage(message: unknown, targetOrigin: string): void;
|
|
44
|
+
} | null;
|
|
45
|
+
readonly clientWidth: number;
|
|
46
|
+
readonly clientHeight: number;
|
|
47
|
+
}
|
|
48
|
+
/** The inbound side: what this host needs from `window`. */
|
|
49
|
+
export interface ViewHostEvents {
|
|
50
|
+
addEventListener(type: "message", listener: (event: {
|
|
51
|
+
data: unknown;
|
|
52
|
+
source: unknown;
|
|
53
|
+
}) => void): void;
|
|
54
|
+
removeEventListener(type: "message", listener: (event: {
|
|
55
|
+
data: unknown;
|
|
56
|
+
source: unknown;
|
|
57
|
+
}) => void): void;
|
|
58
|
+
}
|
|
59
|
+
export interface AttachViewHostConfig {
|
|
60
|
+
/**
|
|
61
|
+
* Capabilities to advertise in the initialize result. Default: `{}` —
|
|
62
|
+
* correct for views that ride their own live channel (ggui views do;
|
|
63
|
+
* they boot from their seeded envelope and talk to their pod directly),
|
|
64
|
+
* and the honest floor for everyone else: advertise only what the
|
|
65
|
+
* embedder implements. Exception: wiring {@link onCallTool} advertises
|
|
66
|
+
* `serverTools` automatically — a wired relay IS the implementation —
|
|
67
|
+
* and an explicit `hostCapabilities.serverTools` still wins.
|
|
68
|
+
*/
|
|
69
|
+
hostCapabilities?: McpUiHostCapabilities;
|
|
70
|
+
/** Host identity for the initialize result. */
|
|
71
|
+
hostInfo?: ViewHostInfo;
|
|
72
|
+
/**
|
|
73
|
+
* Extra context merged over the derived defaults (locale from
|
|
74
|
+
* `navigator`, container dimensions from the frame when it has laid
|
|
75
|
+
* out — a 0×0 pre-layout reading is a lie the spec type shouldn't be
|
|
76
|
+
* told). Keys given here win.
|
|
77
|
+
*/
|
|
78
|
+
hostContext?: McpUiHostContext;
|
|
79
|
+
/**
|
|
80
|
+
* The `tools/call` relay — a PRIVILEGE boundary, default off: with no
|
|
81
|
+
* hook, the machine refuses `tools/call` in-band and advertises no
|
|
82
|
+
* `serverTools`. Wire `createMcpUiActionRelay` (or your own) to let the
|
|
83
|
+
* mounted view reach tools over a transport the embedder owns. The
|
|
84
|
+
* request's `name`/`arguments` are VIEW-CONTROLLED wire data — the hook
|
|
85
|
+
* owns allowlisting and validation (`createMcpUiActionRelay` does both).
|
|
86
|
+
*/
|
|
87
|
+
onCallTool?: (request: UiActionRequest) => Promise<McpToolCallResult>;
|
|
88
|
+
/**
|
|
89
|
+
* The mounted resource's `ui://` locator — the scope stamped on every
|
|
90
|
+
* relayed {@link UiActionRequest}. Required for the relay to fire;
|
|
91
|
+
* `<GuueyView>` fills it from the mount automatically.
|
|
92
|
+
*/
|
|
93
|
+
resourceUri?: string;
|
|
94
|
+
/** Observe phase transitions (see {@link ViewHostPhase}). */
|
|
95
|
+
onPhaseChange?: (phase: ViewHostPhase) => void;
|
|
96
|
+
/**
|
|
97
|
+
* How long to wait for `ui/initialize` before declaring
|
|
98
|
+
* `"no-handshake"` (ms). `0` disables the timer. Default 8000 — a view
|
|
99
|
+
* runtime negotiates immediately after parse; this bound exists to turn
|
|
100
|
+
* "blank forever" into a labeled state, not to race slow networks.
|
|
101
|
+
*/
|
|
102
|
+
negotiationTimeoutMs?: number;
|
|
103
|
+
/** Message-event source, injectable for tests. Default: `window`. */
|
|
104
|
+
events?: ViewHostEvents;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Attach the Host role to a mounted view frame. Returns a detach function;
|
|
108
|
+
* call it before the frame unmounts — it stops listening and posts the
|
|
109
|
+
* spec-mannered `ui/resource-teardown` farewell through the CACHED window
|
|
110
|
+
* handle (post-removal, `frame.contentWindow` is already null).
|
|
111
|
+
*/
|
|
112
|
+
export declare function attachViewHost(frame: ViewFrameLike, config?: AttachViewHostConfig): () => void;
|
|
113
|
+
/**
|
|
114
|
+
* The document a {@link McpUiResourcePayload} mounts: `text` verbatim, or
|
|
115
|
+
* `blob` base64-decoded as UTF-8. `undefined` when the payload carries
|
|
116
|
+
* neither — nothing to put in `srcdoc`.
|
|
117
|
+
*/
|
|
118
|
+
export declare function viewDocumentHtml(resource: McpUiResourcePayload): string | undefined;
|
|
119
|
+
//# sourceMappingURL=view-host.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"view-host.d.ts","sourceRoot":"","sources":["../src/view-host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,EAQL,KAAK,aAAa,EAEnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAEL,KAAK,iBAAiB,EAEtB,KAAK,eAAe,EACrB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,KAAK,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAC9F,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAE5D;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,aAAa,EAAE;QAAE,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;IAC7F,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,4DAA4D;AAC5D,MAAM,WAAW,cAAc;IAC7B,gBAAgB,CACd,IAAI,EAAE,SAAS,EACf,QAAQ,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,GAC5D,IAAI,CAAC;IACR,mBAAmB,CACjB,IAAI,EAAE,SAAS,EACf,QAAQ,EAAE,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,GAC5D,IAAI,CAAC;CACT;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;;;;;OAQG;IACH,gBAAgB,CAAC,EAAE,qBAAqB,CAAC;IACzC,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,gBAAgB,CAAC;IAC/B;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACtE;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6DAA6D;IAC7D,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;IAC/C;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,qEAAqE;IACrE,MAAM,CAAC,EAAE,cAAc,CAAC;CACzB;AA6BD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,aAAa,EAAE,MAAM,GAAE,oBAAyB,GAAG,MAAM,IAAI,CAgElG;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,oBAAoB,GAAG,MAAM,GAAG,SAAS,CAcnF"}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `attachViewHost` — the DOM glue around `view-host-protocol.ts`'s pure
|
|
3
|
+
* machine (guuey#186 Gap 1). Framework-agnostic: any embedder with an
|
|
4
|
+
* iframe can play the MCP Apps Host role with one call; `react.tsx` is one
|
|
5
|
+
* convenience composition of exactly this, and a future full transcript
|
|
6
|
+
* renderer composes the same primitive differently.
|
|
7
|
+
*
|
|
8
|
+
* This module is glue by DESIGN — every decision (what to answer, what to
|
|
9
|
+
* refuse, when the negotiation window lapses) lives in the machine, which
|
|
10
|
+
* the Node-only publish gate can test. What genuinely needs a browser is
|
|
11
|
+
* this file's five moves: listen, identity-filter, post, time, detach —
|
|
12
|
+
* covered by the monorepo's Playwright leg (`e2e/`), which the guuey-sdks
|
|
13
|
+
* mirror deliberately does not carry.
|
|
14
|
+
*
|
|
15
|
+
* ## The identity filter (security invariant)
|
|
16
|
+
*
|
|
17
|
+
* Messages are matched by `event.source === frame.contentWindow`, NEVER by
|
|
18
|
+
* `event.origin`: a view frame runs `sandbox="allow-scripts"` WITHOUT
|
|
19
|
+
* `allow-same-origin`, so its origin is opaque — every message it posts
|
|
20
|
+
* carries `"null"`, a value every other sandboxed frame on the page
|
|
21
|
+
* shares, identifying nobody. The window handle is the only identity that
|
|
22
|
+
* names the frame; a frame with no `contentWindow` matches nothing rather
|
|
23
|
+
* than everything. Responses target `'*'` for the same reason: an opaque
|
|
24
|
+
* origin is not addressable by name, and the handshake payload carries no
|
|
25
|
+
* secrets — it is the result the spec defines for any host.
|
|
26
|
+
*
|
|
27
|
+
* Seeded from ggui's console `surface-host.ts` (donated, guuey#186 audit);
|
|
28
|
+
* re-derived here against the pure machine + our own tests.
|
|
29
|
+
*/
|
|
30
|
+
import { initialViewHostState, teardownMessage, toolCallResponse, viewHostElapsed, viewHostReceive, } from "./view-host-protocol.js";
|
|
31
|
+
import { unavailableToolCallResult, } from "./action.js";
|
|
32
|
+
const DEFAULT_HOST_INFO = { name: "guuey-view-host", version: "1" };
|
|
33
|
+
const DEFAULT_NEGOTIATION_TIMEOUT_MS = 8000;
|
|
34
|
+
/** Derived + configured context, per the {@link AttachViewHostConfig.hostContext} contract. */
|
|
35
|
+
function hostContextFor(frame, config) {
|
|
36
|
+
return {
|
|
37
|
+
locale: typeof navigator !== "undefined" ? navigator.language : "en-US",
|
|
38
|
+
...(frame.clientWidth > 0 && frame.clientHeight > 0
|
|
39
|
+
? { containerDimensions: { width: frame.clientWidth, height: frame.clientHeight } }
|
|
40
|
+
: {}),
|
|
41
|
+
...config.hostContext,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function behaviorFor(frame, config) {
|
|
45
|
+
const relayWired = config.onCallTool !== undefined && config.resourceUri !== undefined;
|
|
46
|
+
return {
|
|
47
|
+
hostInfo: config.hostInfo ?? DEFAULT_HOST_INFO,
|
|
48
|
+
hostCapabilities: {
|
|
49
|
+
...(relayWired ? { serverTools: {} } : {}),
|
|
50
|
+
...config.hostCapabilities,
|
|
51
|
+
},
|
|
52
|
+
hostContext: hostContextFor(frame, config),
|
|
53
|
+
toolRelay: relayWired,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Attach the Host role to a mounted view frame. Returns a detach function;
|
|
58
|
+
* call it before the frame unmounts — it stops listening and posts the
|
|
59
|
+
* spec-mannered `ui/resource-teardown` farewell through the CACHED window
|
|
60
|
+
* handle (post-removal, `frame.contentWindow` is already null).
|
|
61
|
+
*/
|
|
62
|
+
export function attachViewHost(frame, config = {}) {
|
|
63
|
+
const cachedWindow = frame.contentWindow;
|
|
64
|
+
let state = initialViewHostState();
|
|
65
|
+
const setState = (next) => {
|
|
66
|
+
const phaseChanged = next.phase !== state.phase;
|
|
67
|
+
state = next;
|
|
68
|
+
if (phaseChanged)
|
|
69
|
+
config.onPhaseChange?.(next.phase);
|
|
70
|
+
};
|
|
71
|
+
const post = (message) => {
|
|
72
|
+
frame.contentWindow?.postMessage(message, "*");
|
|
73
|
+
};
|
|
74
|
+
const relay = (id, name, args) => {
|
|
75
|
+
const { onCallTool, resourceUri } = config;
|
|
76
|
+
// The machine only emits the effect when the relay is wired (behavior
|
|
77
|
+
// is derived from this same config), so these are invariants, not
|
|
78
|
+
// runtime branches a view can steer.
|
|
79
|
+
if (onCallTool === undefined || resourceUri === undefined)
|
|
80
|
+
return;
|
|
81
|
+
onCallTool({ resourceUri, name, ...(args === undefined ? {} : { arguments: args }) }).then((result) => post(toolCallResponse(id, result)),
|
|
82
|
+
// A relay hook that rejects (createMcpUiActionRelay never does, but
|
|
83
|
+
// the hook is embedder code) still owes the view an answer — the
|
|
84
|
+
// same in-band unavailable the relay itself uses, never a hang.
|
|
85
|
+
() => post(toolCallResponse(id, unavailableToolCallResult())));
|
|
86
|
+
};
|
|
87
|
+
const onMessage = (event) => {
|
|
88
|
+
if (frame.contentWindow === null || event.source !== frame.contentWindow)
|
|
89
|
+
return;
|
|
90
|
+
const { state: next, effects } = viewHostReceive(state, behaviorFor(frame, config), event.data);
|
|
91
|
+
setState(next);
|
|
92
|
+
for (const effect of effects) {
|
|
93
|
+
if (effect.kind === "respond")
|
|
94
|
+
post(effect.message);
|
|
95
|
+
else
|
|
96
|
+
relay(effect.id, effect.name, effect.arguments);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
// One listener, two subscription paths: the injectable seam for Node
|
|
100
|
+
// tests, and `window` — whose lib.dom listener typing wants the concrete
|
|
101
|
+
// `MessageEvent` — for the browser default.
|
|
102
|
+
const subscribe = () => {
|
|
103
|
+
const { events } = config;
|
|
104
|
+
if (events !== undefined) {
|
|
105
|
+
events.addEventListener("message", onMessage);
|
|
106
|
+
return () => events.removeEventListener("message", onMessage);
|
|
107
|
+
}
|
|
108
|
+
const domListener = (event) => onMessage(event);
|
|
109
|
+
window.addEventListener("message", domListener);
|
|
110
|
+
return () => window.removeEventListener("message", domListener);
|
|
111
|
+
};
|
|
112
|
+
const unsubscribe = subscribe();
|
|
113
|
+
const timeoutMs = config.negotiationTimeoutMs ?? DEFAULT_NEGOTIATION_TIMEOUT_MS;
|
|
114
|
+
const timer = timeoutMs > 0 ? setTimeout(() => setState(viewHostElapsed(state)), timeoutMs) : undefined;
|
|
115
|
+
return () => {
|
|
116
|
+
if (timer !== undefined)
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
unsubscribe();
|
|
119
|
+
cachedWindow?.postMessage(teardownMessage(), "*");
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The document a {@link McpUiResourcePayload} mounts: `text` verbatim, or
|
|
124
|
+
* `blob` base64-decoded as UTF-8. `undefined` when the payload carries
|
|
125
|
+
* neither — nothing to put in `srcdoc`.
|
|
126
|
+
*/
|
|
127
|
+
export function viewDocumentHtml(resource) {
|
|
128
|
+
if (typeof resource.text === "string")
|
|
129
|
+
return resource.text;
|
|
130
|
+
if (typeof resource.blob === "string") {
|
|
131
|
+
try {
|
|
132
|
+
const bytes = Uint8Array.from(atob(resource.blob), (c) => c.charCodeAt(0));
|
|
133
|
+
return new TextDecoder().decode(bytes);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// Malformed base64 is producer-side wire data, not an embedder bug —
|
|
137
|
+
// the honest answer is "no document" (the same labeled state a
|
|
138
|
+
// payload with neither field gets), not a render-time throw.
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guuey/mcp-apps-host",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "The MCP Apps (SEP-1865) Host role for guuey's chat surfaces — view-mount narrowing across UI channels, ui:// locator rehydration by resources/read, and the sandbox-trust channel contract. Vendor-neutral: any spec-following MCP App mounts through it.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -24,14 +24,31 @@
|
|
|
24
24
|
"types": "./dist/narrowing.d.ts",
|
|
25
25
|
"import": "./dist/narrowing.js",
|
|
26
26
|
"default": "./dist/narrowing.js"
|
|
27
|
+
},
|
|
28
|
+
"./react": {
|
|
29
|
+
"react-native": "./src/react.tsx",
|
|
30
|
+
"types": "./dist/react.d.ts",
|
|
31
|
+
"import": "./dist/react.js",
|
|
32
|
+
"default": "./dist/react.js"
|
|
27
33
|
}
|
|
28
34
|
},
|
|
29
35
|
"dependencies": {
|
|
30
36
|
"@ggui-ai/protocol": "0.9.0",
|
|
37
|
+
"@modelcontextprotocol/ext-apps": "1.7.5",
|
|
31
38
|
"@silverprotocol/core": "0.4.1"
|
|
32
39
|
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"react": ">=18"
|
|
42
|
+
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"react": {
|
|
45
|
+
"optional": true
|
|
46
|
+
}
|
|
47
|
+
},
|
|
33
48
|
"devDependencies": {
|
|
34
49
|
"@types/node": "^24.0.0",
|
|
50
|
+
"@types/react": "^19.0.0",
|
|
51
|
+
"react": "^19.0.0",
|
|
35
52
|
"typescript": "^5.0.0",
|
|
36
53
|
"vitest": "^3.0.0"
|
|
37
54
|
},
|