@guuey/mcp-apps-host 0.13.0 → 0.14.1
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/dist/action-staging.d.ts +64 -0
- package/dist/action-staging.d.ts.map +1 -0
- package/dist/action-staging.js +112 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/package.json +3 -3
- package/src/action-staging.ts +128 -0
- package/src/index.ts +7 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The post-turn action-STAGING policy (guuey#356 — lifted VERBATIM from the
|
|
3
|
+
* widget's #198/#215/#218 civilization so every mount shares ONE copy).
|
|
4
|
+
*
|
|
5
|
+
* The beat it solves: after a turn completes there is no live turn to feed
|
|
6
|
+
* a card action into — relaying would only surface the "agent not
|
|
7
|
+
* listening" degradation at exactly the moment the agent's own copy
|
|
8
|
+
* invited the tap. Instead, an ALLOWLISTED semantic action with an honest
|
|
9
|
+
* text form stages a human-readable projection into the composer: one
|
|
10
|
+
* Enter sends it as the next turn. Everything else — internal transport
|
|
11
|
+
* rungs, foreign plumbing, shapes with no honest projection — rides the
|
|
12
|
+
* exact pre-#198 relay/stub path, so fallback chains keep receiving the
|
|
13
|
+
* in-band answers their rungs expect and raw plumbing never reaches the
|
|
14
|
+
* composer (guuey#218's prod defect class).
|
|
15
|
+
*
|
|
16
|
+
* A staged answer is deliberately NOT `isError` (guuey#215): queueing is
|
|
17
|
+
* an acceptance, and the error flag fed apps' legacy failure overlays.
|
|
18
|
+
*/
|
|
19
|
+
import { type McpToolCallResult } from "./action.js";
|
|
20
|
+
import type { UiActionRequest } from "./action.js";
|
|
21
|
+
/** The staged-acceptance notice a view receives instead of a relay answer. */
|
|
22
|
+
export declare const ACTION_STAGED_MSG = "Queued \u2014 press Send to continue.";
|
|
23
|
+
/**
|
|
24
|
+
* guuey#198: the post-turn action policy's two host callbacks — when no
|
|
25
|
+
* turn is live, a card action stages into the composer instead of dying in
|
|
26
|
+
* the relay's degradation.
|
|
27
|
+
*/
|
|
28
|
+
export interface ActionStaging {
|
|
29
|
+
/** A turn is currently in flight — the live relay path applies. */
|
|
30
|
+
isTurnLive: () => boolean;
|
|
31
|
+
/** Prefill the composer with the projection (append, never clobber) and focus it. */
|
|
32
|
+
stage: (text: string) => void;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* guuey#218: ONLY an allowlisted semantic action may stage — keyed on the
|
|
36
|
+
* SEMANTIC set ({@link UI_SEMANTIC_ACTION_TOOLS}), never the wider relay
|
|
37
|
+
* allowlist, so a relayable transport rung is never stage-eligible.
|
|
38
|
+
*/
|
|
39
|
+
export declare function isStageableAction(name: string): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* The human-readable projection of a card action — what lands in the
|
|
42
|
+
* composer as the user's next message (guuey#198, option (b)). `null`
|
|
43
|
+
* when the action shape has no honest text form (nested/non-primitive
|
|
44
|
+
* arguments): projecting only part of the arguments could stage a
|
|
45
|
+
* MISLEADING selection, so those degrade to the relay/stub path instead.
|
|
46
|
+
*
|
|
47
|
+
* guuey#215: a semantic-envelope call carries the USER'S action inside its
|
|
48
|
+
* arguments (`actionId` + the action's own params), so the projection
|
|
49
|
+
* unwraps the envelope — humanizing the carrier name would stage plumbing,
|
|
50
|
+
* not the user's words. No string `actionId` ⇒ no honest projection ⇒
|
|
51
|
+
* relay path.
|
|
52
|
+
*/
|
|
53
|
+
export declare function stagedActionText(name: string, args: unknown): string | null;
|
|
54
|
+
/**
|
|
55
|
+
* Wrap ANY `onCallTool`-shaped hook with the staging policy (guuey#356):
|
|
56
|
+
* post-turn + allowlisted + honest projection → stage and answer the
|
|
57
|
+
* STAGED notice; every other call — mid-turn, non-semantic, or
|
|
58
|
+
* projection-less — delegates to `inner` byte-identically. A mount passes
|
|
59
|
+
* two callbacks ({@link ActionStaging}) and gets the widget's exact
|
|
60
|
+
* ratified behavior; `staging` absent returns `inner` unchanged, so the
|
|
61
|
+
* wrap composes freely at construction sites.
|
|
62
|
+
*/
|
|
63
|
+
export declare function withActionStaging(inner: (request: UiActionRequest) => Promise<McpToolCallResult>, staging: ActionStaging | undefined): (request: UiActionRequest) => Promise<McpToolCallResult>;
|
|
64
|
+
//# sourceMappingURL=action-staging.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"action-staging.d.ts","sourceRoot":"","sources":["../src/action-staging.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAA4B,KAAK,iBAAiB,EAAiC,MAAM,aAAa,CAAC;AAC9G,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD,8EAA8E;AAC9E,eAAO,MAAM,iBAAiB,0CAAqC,CAAC;AAEpE;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,mEAAmE;IACnE,UAAU,EAAE,MAAM,OAAO,CAAC;IAC1B,qFAAqF;IACrF,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/B;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEvD;AAiCD;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAY3E;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,OAAO,CAAC,iBAAiB,CAAC,EAC/D,OAAO,EAAE,aAAa,GAAG,SAAS,GACjC,CAAC,OAAO,EAAE,eAAe,KAAK,OAAO,CAAC,iBAAiB,CAAC,CAY1D"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The post-turn action-STAGING policy (guuey#356 — lifted VERBATIM from the
|
|
3
|
+
* widget's #198/#215/#218 civilization so every mount shares ONE copy).
|
|
4
|
+
*
|
|
5
|
+
* The beat it solves: after a turn completes there is no live turn to feed
|
|
6
|
+
* a card action into — relaying would only surface the "agent not
|
|
7
|
+
* listening" degradation at exactly the moment the agent's own copy
|
|
8
|
+
* invited the tap. Instead, an ALLOWLISTED semantic action with an honest
|
|
9
|
+
* text form stages a human-readable projection into the composer: one
|
|
10
|
+
* Enter sends it as the next turn. Everything else — internal transport
|
|
11
|
+
* rungs, foreign plumbing, shapes with no honest projection — rides the
|
|
12
|
+
* exact pre-#198 relay/stub path, so fallback chains keep receiving the
|
|
13
|
+
* in-band answers their rungs expect and raw plumbing never reaches the
|
|
14
|
+
* composer (guuey#218's prod defect class).
|
|
15
|
+
*
|
|
16
|
+
* A staged answer is deliberately NOT `isError` (guuey#215): queueing is
|
|
17
|
+
* an acceptance, and the error flag fed apps' legacy failure overlays.
|
|
18
|
+
*/
|
|
19
|
+
import { UI_SEMANTIC_ACTION_TOOLS } from "./action.js";
|
|
20
|
+
/** The staged-acceptance notice a view receives instead of a relay answer. */
|
|
21
|
+
export const ACTION_STAGED_MSG = "Queued — press Send to continue.";
|
|
22
|
+
/**
|
|
23
|
+
* guuey#218: ONLY an allowlisted semantic action may stage — keyed on the
|
|
24
|
+
* SEMANTIC set ({@link UI_SEMANTIC_ACTION_TOOLS}), never the wider relay
|
|
25
|
+
* allowlist, so a relayable transport rung is never stage-eligible.
|
|
26
|
+
*/
|
|
27
|
+
export function isStageableAction(name) {
|
|
28
|
+
return UI_SEMANTIC_ACTION_TOOLS.has(name);
|
|
29
|
+
}
|
|
30
|
+
/** "selectSlot" → "Select slot"; "book_slot" → "Book slot". */
|
|
31
|
+
function humanizeActionName(name) {
|
|
32
|
+
const words = name
|
|
33
|
+
.replace(/[_-]+/g, " ")
|
|
34
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
35
|
+
.toLowerCase()
|
|
36
|
+
.trim();
|
|
37
|
+
return words.length === 0 ? name : words[0].toUpperCase() + words.slice(1);
|
|
38
|
+
}
|
|
39
|
+
/** The protocol-open wire shape — same guard idiom as action.ts's. */
|
|
40
|
+
function isStructuredArgs(value) {
|
|
41
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
42
|
+
}
|
|
43
|
+
function projectAction(name, args) {
|
|
44
|
+
const title = humanizeActionName(name);
|
|
45
|
+
if (args === undefined || args === null)
|
|
46
|
+
return title;
|
|
47
|
+
if (typeof args !== "object" || Array.isArray(args))
|
|
48
|
+
return null;
|
|
49
|
+
const entries = Object.entries(args);
|
|
50
|
+
if (entries.length === 0)
|
|
51
|
+
return title;
|
|
52
|
+
const pairs = [];
|
|
53
|
+
for (const [key, value] of entries) {
|
|
54
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
pairs.push(`${key} ${String(value)}`);
|
|
58
|
+
}
|
|
59
|
+
return `${title}: ${pairs.join(", ")}`;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The human-readable projection of a card action — what lands in the
|
|
63
|
+
* composer as the user's next message (guuey#198, option (b)). `null`
|
|
64
|
+
* when the action shape has no honest text form (nested/non-primitive
|
|
65
|
+
* arguments): projecting only part of the arguments could stage a
|
|
66
|
+
* MISLEADING selection, so those degrade to the relay/stub path instead.
|
|
67
|
+
*
|
|
68
|
+
* guuey#215: a semantic-envelope call carries the USER'S action inside its
|
|
69
|
+
* arguments (`actionId` + the action's own params), so the projection
|
|
70
|
+
* unwraps the envelope — humanizing the carrier name would stage plumbing,
|
|
71
|
+
* not the user's words. No string `actionId` ⇒ no honest projection ⇒
|
|
72
|
+
* relay path.
|
|
73
|
+
*/
|
|
74
|
+
export function stagedActionText(name, args) {
|
|
75
|
+
if (UI_SEMANTIC_ACTION_TOOLS.has(name)) {
|
|
76
|
+
if (!isStructuredArgs(args))
|
|
77
|
+
return null;
|
|
78
|
+
const actionId = args["actionId"];
|
|
79
|
+
if (typeof actionId !== "string" || actionId.length === 0)
|
|
80
|
+
return null;
|
|
81
|
+
const rest = {};
|
|
82
|
+
for (const [key, value] of Object.entries(args)) {
|
|
83
|
+
if (key !== "actionId")
|
|
84
|
+
rest[key] = value;
|
|
85
|
+
}
|
|
86
|
+
return projectAction(actionId, rest);
|
|
87
|
+
}
|
|
88
|
+
return projectAction(name, args);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Wrap ANY `onCallTool`-shaped hook with the staging policy (guuey#356):
|
|
92
|
+
* post-turn + allowlisted + honest projection → stage and answer the
|
|
93
|
+
* STAGED notice; every other call — mid-turn, non-semantic, or
|
|
94
|
+
* projection-less — delegates to `inner` byte-identically. A mount passes
|
|
95
|
+
* two callbacks ({@link ActionStaging}) and gets the widget's exact
|
|
96
|
+
* ratified behavior; `staging` absent returns `inner` unchanged, so the
|
|
97
|
+
* wrap composes freely at construction sites.
|
|
98
|
+
*/
|
|
99
|
+
export function withActionStaging(inner, staging) {
|
|
100
|
+
if (staging === undefined)
|
|
101
|
+
return inner;
|
|
102
|
+
return async (request) => {
|
|
103
|
+
if (!staging.isTurnLive() && isStageableAction(request.name)) {
|
|
104
|
+
const text = stagedActionText(request.name, request.arguments);
|
|
105
|
+
if (text !== null) {
|
|
106
|
+
staging.stage(text);
|
|
107
|
+
return { content: [{ type: "text", text: ACTION_STAGED_MSG }] };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return inner(request);
|
|
111
|
+
};
|
|
112
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { asResourcePayload, asUiResource, blockUiResource, isJsonObject, resourceHtml, scanProviderRawForUiResource, snapshotUiResource, toolResultLocator, toolResultUiResource, uiLocator, type McpUiResourcePayload, } from "./block-ui.js";
|
|
7
7
|
export { resolveViewMount, snapshotViewMount, toolResultViewMount, type LocatorViewMount, type ResolvedViewMount, type UiResourceReader, type ViewMount, type ViewMountChannel, } from "./card-mount.js";
|
|
8
|
+
export { ACTION_STAGED_MSG, isStageableAction, stagedActionText, withActionStaging, type ActionStaging, } from "./action-staging.js";
|
|
8
9
|
export { createMcpUiResourceReader, declaredResourceCsp, uiResourceChannel, type CreateMcpUiResourceReaderDeps, type McpResourceReadResult, } from "./reader.js";
|
|
9
10
|
export type { McpUiResourceCsp } from "@modelcontextprotocol/ext-apps";
|
|
10
11
|
export { asToolCallResult, createMcpUiActionRelay, unavailableToolCallResult, UI_ACTION_TOOLS, UI_ACTION_UNAVAILABLE_TEXT, UI_SEMANTIC_ACTION_TOOLS, type CreateMcpUiActionRelayDeps, type McpToolCallContent, type McpToolCallResult, type McpToolStructuredContent, type UiActionRequest, } from "./action.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,4BAA4B,EAC5B,kBAAkB,EAClB,iBAAiB,EACjB,oBAAoB,EACpB,SAAS,EACT,KAAK,oBAAoB,GAC1B,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,SAAS,EACd,KAAK,gBAAgB,GACtB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,yBAAyB,EACzB,mBAAmB,EACnB,iBAAiB,EACjB,KAAK,6BAA6B,EAClC,KAAK,qBAAqB,GAC3B,MAAM,aAAa,CAAC;AAIrB,YAAY,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,yBAAyB,EACzB,eAAe,EACf,0BAA0B,EAC1B,wBAAwB,EACxB,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,oBAAoB,EACpB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,aAAa,GACnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,cAAc,GACpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,yBAAyB,EACzB,mBAAmB,EACnB,0BAA0B,EAC1B,6BAA6B,EAC7B,KAAK,yBAAyB,GAC/B,MAAM,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,4BAA4B,EAC5B,kBAAkB,EAClB,iBAAiB,EACjB,oBAAoB,EACpB,SAAS,EACT,KAAK,oBAAoB,GAC1B,MAAM,eAAe,CAAC;AACvB,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,SAAS,EACd,KAAK,gBAAgB,GACtB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EACjB,KAAK,aAAa,GACnB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,yBAAyB,EACzB,mBAAmB,EACnB,iBAAiB,EACjB,KAAK,6BAA6B,EAClC,KAAK,qBAAqB,GAC3B,MAAM,aAAa,CAAC;AAIrB,YAAY,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,yBAAyB,EACzB,eAAe,EACf,0BAA0B,EAC1B,wBAAwB,EACxB,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,EACf,eAAe,EACf,oBAAoB,EACpB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,aAAa,GACnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,cAAc,EACd,gBAAgB,EAChB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,cAAc,GACpB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACL,yBAAyB,EACzB,mBAAmB,EACnB,0BAA0B,EAC1B,6BAA6B,EAC7B,KAAK,yBAAyB,GAC/B,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export { asResourcePayload, asUiResource, blockUiResource, isJsonObject, resourceHtml, scanProviderRawForUiResource, snapshotUiResource, toolResultLocator, toolResultUiResource, uiLocator, } from "./block-ui.js";
|
|
7
7
|
export { resolveViewMount, snapshotViewMount, toolResultViewMount, } from "./card-mount.js";
|
|
8
|
+
export { ACTION_STAGED_MSG, isStageableAction, stagedActionText, withActionStaging, } from "./action-staging.js";
|
|
8
9
|
export { createMcpUiResourceReader, declaredResourceCsp, uiResourceChannel, } from "./reader.js";
|
|
9
10
|
export { asToolCallResult, createMcpUiActionRelay, unavailableToolCallResult, UI_ACTION_TOOLS, UI_ACTION_UNAVAILABLE_TEXT, UI_SEMANTIC_ACTION_TOOLS, } from "./action.js";
|
|
10
11
|
export { initializeResult, initialViewHostState, resourceReadResponse, RESOURCES_READ_METHOD, teardownMessage, toolCallResponse, TOOLS_CALL_METHOD, viewHostElapsed, viewHostReceive, diagnoseCspViolation, } from "./view-host-protocol.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guuey/mcp-apps-host",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.1",
|
|
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",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@ggui-ai/protocol": "0.
|
|
36
|
+
"@ggui-ai/protocol": "0.12.0",
|
|
37
37
|
"@modelcontextprotocol/ext-apps": "^1.7.5",
|
|
38
38
|
"@silverprotocol/core": "0.5.1"
|
|
39
39
|
},
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"@types/react": "^19.0.0",
|
|
51
51
|
"react": "^19.0.0",
|
|
52
52
|
"typescript": "^5.0.0",
|
|
53
|
-
"vitest": "^3.
|
|
53
|
+
"vitest": "^3.2.7"
|
|
54
54
|
},
|
|
55
55
|
"publishConfig": {
|
|
56
56
|
"access": "public"
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The post-turn action-STAGING policy (guuey#356 — lifted VERBATIM from the
|
|
3
|
+
* widget's #198/#215/#218 civilization so every mount shares ONE copy).
|
|
4
|
+
*
|
|
5
|
+
* The beat it solves: after a turn completes there is no live turn to feed
|
|
6
|
+
* a card action into — relaying would only surface the "agent not
|
|
7
|
+
* listening" degradation at exactly the moment the agent's own copy
|
|
8
|
+
* invited the tap. Instead, an ALLOWLISTED semantic action with an honest
|
|
9
|
+
* text form stages a human-readable projection into the composer: one
|
|
10
|
+
* Enter sends it as the next turn. Everything else — internal transport
|
|
11
|
+
* rungs, foreign plumbing, shapes with no honest projection — rides the
|
|
12
|
+
* exact pre-#198 relay/stub path, so fallback chains keep receiving the
|
|
13
|
+
* in-band answers their rungs expect and raw plumbing never reaches the
|
|
14
|
+
* composer (guuey#218's prod defect class).
|
|
15
|
+
*
|
|
16
|
+
* A staged answer is deliberately NOT `isError` (guuey#215): queueing is
|
|
17
|
+
* an acceptance, and the error flag fed apps' legacy failure overlays.
|
|
18
|
+
*/
|
|
19
|
+
import { UI_SEMANTIC_ACTION_TOOLS, type McpToolCallResult, type McpToolStructuredContent } from "./action.js";
|
|
20
|
+
import type { UiActionRequest } from "./action.js";
|
|
21
|
+
|
|
22
|
+
/** The staged-acceptance notice a view receives instead of a relay answer. */
|
|
23
|
+
export const ACTION_STAGED_MSG = "Queued — press Send to continue.";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* guuey#198: the post-turn action policy's two host callbacks — when no
|
|
27
|
+
* turn is live, a card action stages into the composer instead of dying in
|
|
28
|
+
* the relay's degradation.
|
|
29
|
+
*/
|
|
30
|
+
export interface ActionStaging {
|
|
31
|
+
/** A turn is currently in flight — the live relay path applies. */
|
|
32
|
+
isTurnLive: () => boolean;
|
|
33
|
+
/** Prefill the composer with the projection (append, never clobber) and focus it. */
|
|
34
|
+
stage: (text: string) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* guuey#218: ONLY an allowlisted semantic action may stage — keyed on the
|
|
39
|
+
* SEMANTIC set ({@link UI_SEMANTIC_ACTION_TOOLS}), never the wider relay
|
|
40
|
+
* allowlist, so a relayable transport rung is never stage-eligible.
|
|
41
|
+
*/
|
|
42
|
+
export function isStageableAction(name: string): boolean {
|
|
43
|
+
return UI_SEMANTIC_ACTION_TOOLS.has(name);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** "selectSlot" → "Select slot"; "book_slot" → "Book slot". */
|
|
47
|
+
function humanizeActionName(name: string): string {
|
|
48
|
+
const words = name
|
|
49
|
+
.replace(/[_-]+/g, " ")
|
|
50
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
51
|
+
.toLowerCase()
|
|
52
|
+
.trim();
|
|
53
|
+
return words.length === 0 ? name : words[0].toUpperCase() + words.slice(1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The protocol-open wire shape — same guard idiom as action.ts's. */
|
|
57
|
+
function isStructuredArgs(value: unknown): value is McpToolStructuredContent {
|
|
58
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function projectAction(name: string, args: unknown): string | null {
|
|
62
|
+
const title = humanizeActionName(name);
|
|
63
|
+
if (args === undefined || args === null) return title;
|
|
64
|
+
if (typeof args !== "object" || Array.isArray(args)) return null;
|
|
65
|
+
const entries = Object.entries(args);
|
|
66
|
+
if (entries.length === 0) return title;
|
|
67
|
+
const pairs: string[] = [];
|
|
68
|
+
for (const [key, value] of entries) {
|
|
69
|
+
if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
pairs.push(`${key} ${String(value)}`);
|
|
73
|
+
}
|
|
74
|
+
return `${title}: ${pairs.join(", ")}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The human-readable projection of a card action — what lands in the
|
|
79
|
+
* composer as the user's next message (guuey#198, option (b)). `null`
|
|
80
|
+
* when the action shape has no honest text form (nested/non-primitive
|
|
81
|
+
* arguments): projecting only part of the arguments could stage a
|
|
82
|
+
* MISLEADING selection, so those degrade to the relay/stub path instead.
|
|
83
|
+
*
|
|
84
|
+
* guuey#215: a semantic-envelope call carries the USER'S action inside its
|
|
85
|
+
* arguments (`actionId` + the action's own params), so the projection
|
|
86
|
+
* unwraps the envelope — humanizing the carrier name would stage plumbing,
|
|
87
|
+
* not the user's words. No string `actionId` ⇒ no honest projection ⇒
|
|
88
|
+
* relay path.
|
|
89
|
+
*/
|
|
90
|
+
export function stagedActionText(name: string, args: unknown): string | null {
|
|
91
|
+
if (UI_SEMANTIC_ACTION_TOOLS.has(name)) {
|
|
92
|
+
if (!isStructuredArgs(args)) return null;
|
|
93
|
+
const actionId = args["actionId"];
|
|
94
|
+
if (typeof actionId !== "string" || actionId.length === 0) return null;
|
|
95
|
+
const rest: McpToolStructuredContent = {};
|
|
96
|
+
for (const [key, value] of Object.entries(args)) {
|
|
97
|
+
if (key !== "actionId") rest[key] = value;
|
|
98
|
+
}
|
|
99
|
+
return projectAction(actionId, rest);
|
|
100
|
+
}
|
|
101
|
+
return projectAction(name, args);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Wrap ANY `onCallTool`-shaped hook with the staging policy (guuey#356):
|
|
106
|
+
* post-turn + allowlisted + honest projection → stage and answer the
|
|
107
|
+
* STAGED notice; every other call — mid-turn, non-semantic, or
|
|
108
|
+
* projection-less — delegates to `inner` byte-identically. A mount passes
|
|
109
|
+
* two callbacks ({@link ActionStaging}) and gets the widget's exact
|
|
110
|
+
* ratified behavior; `staging` absent returns `inner` unchanged, so the
|
|
111
|
+
* wrap composes freely at construction sites.
|
|
112
|
+
*/
|
|
113
|
+
export function withActionStaging(
|
|
114
|
+
inner: (request: UiActionRequest) => Promise<McpToolCallResult>,
|
|
115
|
+
staging: ActionStaging | undefined,
|
|
116
|
+
): (request: UiActionRequest) => Promise<McpToolCallResult> {
|
|
117
|
+
if (staging === undefined) return inner;
|
|
118
|
+
return async (request) => {
|
|
119
|
+
if (!staging.isTurnLive() && isStageableAction(request.name)) {
|
|
120
|
+
const text = stagedActionText(request.name, request.arguments);
|
|
121
|
+
if (text !== null) {
|
|
122
|
+
staging.stage(text);
|
|
123
|
+
return { content: [{ type: "text", text: ACTION_STAGED_MSG }] };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return inner(request);
|
|
127
|
+
};
|
|
128
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -26,6 +26,13 @@ export {
|
|
|
26
26
|
type ViewMount,
|
|
27
27
|
type ViewMountChannel,
|
|
28
28
|
} from "./card-mount.js";
|
|
29
|
+
export {
|
|
30
|
+
ACTION_STAGED_MSG,
|
|
31
|
+
isStageableAction,
|
|
32
|
+
stagedActionText,
|
|
33
|
+
withActionStaging,
|
|
34
|
+
type ActionStaging,
|
|
35
|
+
} from "./action-staging.js";
|
|
29
36
|
export {
|
|
30
37
|
createMcpUiResourceReader,
|
|
31
38
|
declaredResourceCsp,
|