@guuey/agent-client 0.6.0 → 0.6.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/web-adapters.d.ts +28 -8
- package/dist/web-adapters.d.ts.map +1 -1
- package/dist/web-adapters.js +50 -15
- package/package.json +2 -2
- package/src/web-adapters.ts +65 -15
package/dist/web-adapters.d.ts
CHANGED
|
@@ -95,6 +95,18 @@ export interface CreateUiResourceReaderOptions {
|
|
|
95
95
|
apiBaseUrl: string;
|
|
96
96
|
/** The thread whose persisted locators this reader may resolve. */
|
|
97
97
|
threadId: string;
|
|
98
|
+
/**
|
|
99
|
+
* The pod base (or full invoke URL — same normalization as the invoke
|
|
100
|
+
* transport). When set, the reader tries the POD door first
|
|
101
|
+
* (`GET <base>/agent/ui-resource`, guuey#209 C1): the pod is the only
|
|
102
|
+
* party that can vouch for a locator whose turn is still streaming —
|
|
103
|
+
* persisted `kind:'card'` rows land at turn COMPLETION, so the platform
|
|
104
|
+
* door 404s mid-turn by construction. Completed turns 404 on the pod
|
|
105
|
+
* (past its grace window) and resolve on the platform door instead: one
|
|
106
|
+
* authority per lifecycle phase, and this reader tries both in that
|
|
107
|
+
* order. Absent → platform door only (pre-#209 behavior).
|
|
108
|
+
*/
|
|
109
|
+
endpointUrl?: string | null;
|
|
98
110
|
/** Signed-in bearer — wins over the guest secret (same rule as the transport). */
|
|
99
111
|
getAccessToken?: (opts?: {
|
|
100
112
|
forceRefresh?: boolean;
|
|
@@ -105,18 +117,26 @@ export interface CreateUiResourceReaderOptions {
|
|
|
105
117
|
fetchImpl?: typeof fetch;
|
|
106
118
|
}
|
|
107
119
|
/**
|
|
108
|
-
* Build a `UiResourceReader` over guuey's authenticated resources/read
|
|
109
|
-
* (guuey#
|
|
120
|
+
* Build a `UiResourceReader` over guuey's authenticated resources/read
|
|
121
|
+
* doors — the pod door for LIVE turns (guuey#209 C1:
|
|
122
|
+
* `GET <pod>/agent/ui-resource?uri=…`, when {@link CreateUiResourceReaderOptions.endpointUrl}
|
|
123
|
+
* is set) and the platform proxy for persisted locators (guuey#122 Gap 1:
|
|
124
|
+
* `GET /v1/threads/:threadId/ui-resource?uri=…`). Both doors answer the
|
|
125
|
+
* same body and speak the same identity (bearer wins, guest header
|
|
126
|
+
* otherwise — the pod's `resolveIdentity` and the proxy's identity chain
|
|
127
|
+
* accept the identical carriers), so one parse serves both.
|
|
110
128
|
*
|
|
111
129
|
* This is `@guuey/mcp-apps-host`'s `createMcpUiResourceReader` assembly over
|
|
112
130
|
* a guuey-platform transport (guuey#127) — channel resolution and payload
|
|
113
131
|
* narrowing live in the host package; only the transport is guuey-shaped.
|
|
114
|
-
* The
|
|
115
|
-
* families as the history read),
|
|
116
|
-
* scope guard, and the
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
132
|
+
* The doors own EVERYTHING trust-shaped: caller identity (the same three
|
|
133
|
+
* families as the history read), tenancy (the pod's live-card ledger; the
|
|
134
|
+
* proxy's thread-ownership + locator-to-thread scope guard), and the
|
|
135
|
+
* per-user federation mint. This transport only carries the surface's
|
|
136
|
+
* existing credential and maps EVERY non-OK — 401/403/404/502 alike — to
|
|
137
|
+
* "try the next door", and a miss on the last door to `undefined`: deny is
|
|
138
|
+
* byte-identical to a miss, and a miss renders the host's placeholder,
|
|
139
|
+
* never an error surface.
|
|
120
140
|
*/
|
|
121
141
|
export declare function createUiResourceReader(options: CreateUiResourceReaderOptions): (resourceUri: string) => Promise<ResolvedViewMount | undefined>;
|
|
122
142
|
/** Options for {@link createUiActionRelay} — same credential surface as the reader. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web-adapters.d.ts","sourceRoot":"","sources":["../src/web-adapters.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAIL,KAAK,iBAAiB,EAEtB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACrB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,mBAAmB,EAAmB,aAAa,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"web-adapters.d.ts","sourceRoot":"","sources":["../src/web-adapters.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAIL,KAAK,iBAAiB,EAEtB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACrB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,KAAK,EAAE,mBAAmB,EAAmB,aAAa,EAAE,MAAM,YAAY,CAAC;AAKtF,sEAAsE;AACtE,eAAO,MAAM,uBAAuB,EAAE,aAiBrC,CAAC;AAEF,mEAAmE;AACnE,wBAAgB,aAAa,IAAI,MAAM,CAKtC;AAED,MAAM,WAAW,wBAAwB;IACvC;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;;;;;;;;;;;;OAiBG;IACH,cAAc,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC/E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAuCG;IACH,cAAc,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;CACtC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,GAAE,wBAA6B,GAClC,mBAAmB,CA8ErB;AAED,mGAAmG;AACnG,MAAM,WAAW,6BAA6B;IAC5C,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;;;;;;OAUG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,kFAAkF;IAClF,cAAc,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC/E,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,4BAA4B;IAC5B,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAOD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,6BAA6B,GACrC,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CA4EjE;AAED,uFAAuF;AACvF,MAAM,WAAW,0BAA0B;IACzC,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAC;IACnB,+DAA+D;IAC/D,QAAQ,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,cAAc,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE;QAAE,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC/E,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,4BAA4B;IAC5B,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,0BAA0B,GAClC,CAAC,OAAO,EAAE,eAAe,KAAK,OAAO,CAAC,iBAAiB,CAAC,CA+C1D"}
|
package/dist/web-adapters.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { createMcpUiActionRelay, createMcpUiResourceReader, } from "@guuey/mcp-apps-host";
|
|
10
10
|
import { fetchThreadHistory, HistoryUnauthorizedError } from "./history.js";
|
|
11
11
|
import { fetchStreamTransport, sendableGuestSecret, GUEST_HEADER } from "./transport.js";
|
|
12
|
+
import { toInvokeUrl } from "./invoke-turn.js";
|
|
12
13
|
/** Persists the threadId in `window.localStorage` (synchronously). */
|
|
13
14
|
export const localStorageThreadStore = {
|
|
14
15
|
load(key) {
|
|
@@ -124,24 +125,45 @@ export function createWebAdapters(opts = {}) {
|
|
|
124
125
|
}
|
|
125
126
|
return adapters;
|
|
126
127
|
}
|
|
128
|
+
/** `<pod base>/agent/ui-resource` from whatever endpoint shape the surface holds. */
|
|
129
|
+
function toUiResourceUrl(endpointUrl) {
|
|
130
|
+
return toInvokeUrl(endpointUrl).replace(/\/agent\/invoke$/, "/agent/ui-resource");
|
|
131
|
+
}
|
|
127
132
|
/**
|
|
128
|
-
* Build a `UiResourceReader` over guuey's authenticated resources/read
|
|
129
|
-
* (guuey#
|
|
133
|
+
* Build a `UiResourceReader` over guuey's authenticated resources/read
|
|
134
|
+
* doors — the pod door for LIVE turns (guuey#209 C1:
|
|
135
|
+
* `GET <pod>/agent/ui-resource?uri=…`, when {@link CreateUiResourceReaderOptions.endpointUrl}
|
|
136
|
+
* is set) and the platform proxy for persisted locators (guuey#122 Gap 1:
|
|
137
|
+
* `GET /v1/threads/:threadId/ui-resource?uri=…`). Both doors answer the
|
|
138
|
+
* same body and speak the same identity (bearer wins, guest header
|
|
139
|
+
* otherwise — the pod's `resolveIdentity` and the proxy's identity chain
|
|
140
|
+
* accept the identical carriers), so one parse serves both.
|
|
130
141
|
*
|
|
131
142
|
* This is `@guuey/mcp-apps-host`'s `createMcpUiResourceReader` assembly over
|
|
132
143
|
* a guuey-platform transport (guuey#127) — channel resolution and payload
|
|
133
144
|
* narrowing live in the host package; only the transport is guuey-shaped.
|
|
134
|
-
* The
|
|
135
|
-
* families as the history read),
|
|
136
|
-
* scope guard, and the
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
145
|
+
* The doors own EVERYTHING trust-shaped: caller identity (the same three
|
|
146
|
+
* families as the history read), tenancy (the pod's live-card ledger; the
|
|
147
|
+
* proxy's thread-ownership + locator-to-thread scope guard), and the
|
|
148
|
+
* per-user federation mint. This transport only carries the surface's
|
|
149
|
+
* existing credential and maps EVERY non-OK — 401/403/404/502 alike — to
|
|
150
|
+
* "try the next door", and a miss on the last door to `undefined`: deny is
|
|
151
|
+
* byte-identical to a miss, and a miss renders the host's placeholder,
|
|
152
|
+
* never an error surface.
|
|
140
153
|
*/
|
|
141
154
|
export function createUiResourceReader(options) {
|
|
142
155
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
143
|
-
|
|
156
|
+
/** One door: fetch + the history adapter's 401-forceRefresh recovery + parse. */
|
|
157
|
+
const readDoor = async (requestUrl) => {
|
|
158
|
+
// Exactly ONE identity carrier per read, the invoke transport's rule
|
|
159
|
+
// (`streamInvokeOnce`): bearer → guest header → else cookie credentials,
|
|
160
|
+
// which round-trip the HttpOnly `guuey_guest` cookie the pod mints for
|
|
161
|
+
// anonymous browser callers. Without the third arm a cookie-mode guest
|
|
162
|
+
// sent an identity-less read and every locator rendered as expired
|
|
163
|
+
// (guuey#221). Never two at once: a request carrying either header does
|
|
164
|
+
// NOT also send cookies.
|
|
144
165
|
const headers = {};
|
|
166
|
+
const init = { headers };
|
|
145
167
|
const token = options.getAccessToken ? await options.getAccessToken() : null;
|
|
146
168
|
const guest = sendableGuestSecret(options.guestSecret);
|
|
147
169
|
if (token) {
|
|
@@ -150,17 +172,20 @@ export function createUiResourceReader(options) {
|
|
|
150
172
|
else if (guest) {
|
|
151
173
|
headers[GUEST_HEADER] = guest;
|
|
152
174
|
}
|
|
153
|
-
|
|
175
|
+
else {
|
|
176
|
+
init.credentials = "include";
|
|
177
|
+
}
|
|
154
178
|
let res;
|
|
155
179
|
try {
|
|
156
|
-
res = await fetchImpl(requestUrl,
|
|
180
|
+
res = await fetchImpl(requestUrl, init);
|
|
157
181
|
}
|
|
158
182
|
catch {
|
|
159
|
-
return undefined; // transport failure == miss
|
|
183
|
+
return undefined; // transport failure == miss (the next door may still answer)
|
|
160
184
|
}
|
|
161
185
|
// One forceRefresh retry on 401 with a bearer in play — the same
|
|
162
186
|
// expired-but-refreshable recovery the history adapter performs;
|
|
163
|
-
// without it a stale token degrades to a permanent placeholder.
|
|
187
|
+
// without it a stale token degrades to a permanent placeholder. The
|
|
188
|
+
// retry carries the fresh bearer and nothing else (same one-carrier rule).
|
|
164
189
|
if (res.status === 401 && options.getAccessToken) {
|
|
165
190
|
const fresh = await options.getAccessToken({ forceRefresh: true }).catch(() => null);
|
|
166
191
|
if (fresh) {
|
|
@@ -183,8 +208,8 @@ export function createUiResourceReader(options) {
|
|
|
183
208
|
catch {
|
|
184
209
|
return undefined;
|
|
185
210
|
}
|
|
186
|
-
//
|
|
187
|
-
// silently a miss —
|
|
211
|
+
// Both doors pass the blob arm through (a blob-only resource is not
|
|
212
|
+
// silently a miss — the route contract); mirror that here.
|
|
188
213
|
if (typeof body.uri !== "string")
|
|
189
214
|
return undefined;
|
|
190
215
|
if (typeof body.text !== "string" && typeof body.blob !== "string")
|
|
@@ -196,6 +221,16 @@ export function createUiResourceReader(options) {
|
|
|
196
221
|
...(typeof body.blob === "string" ? { blob: body.blob } : {}),
|
|
197
222
|
};
|
|
198
223
|
};
|
|
224
|
+
const podUrl = options.endpointUrl ? toUiResourceUrl(options.endpointUrl) : null;
|
|
225
|
+
const readResource = async (resourceUri) => {
|
|
226
|
+
const query = `?uri=${encodeURIComponent(resourceUri)}`;
|
|
227
|
+
if (podUrl !== null) {
|
|
228
|
+
const live = await readDoor(`${podUrl}${query}`);
|
|
229
|
+
if (live !== undefined)
|
|
230
|
+
return live;
|
|
231
|
+
}
|
|
232
|
+
return readDoor(`${options.apiBaseUrl}/threads/${encodeURIComponent(options.threadId)}/ui-resource${query}`);
|
|
233
|
+
};
|
|
199
234
|
return createMcpUiResourceReader({ readResource });
|
|
200
235
|
}
|
|
201
236
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guuey/agent-client",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Client SDK for Guuey's agent runtime: the `useAgentInvoke` React hook + pure SSE helpers that speak the /agent/invoke streaming contract, plus the paginated thread-history read plane. Host adapters (storage / id / transport) are injected, so it runs on web (Next) and React Native alike.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@silverprotocol/core": "0.5.0",
|
|
38
|
-
"@guuey/mcp-apps-host": "0.6.
|
|
38
|
+
"@guuey/mcp-apps-host": "0.6.1"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"react": ">=18"
|
package/src/web-adapters.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
import type { AgentInvokeAdapters, InvokeTransport, ThreadIdStore } from "./types.js";
|
|
19
19
|
import { fetchThreadHistory, HistoryUnauthorizedError } from "./history.js";
|
|
20
20
|
import { fetchStreamTransport, sendableGuestSecret, GUEST_HEADER } from "./transport.js";
|
|
21
|
+
import { toInvokeUrl } from "./invoke-turn.js";
|
|
21
22
|
|
|
22
23
|
/** Persists the threadId in `window.localStorage` (synchronously). */
|
|
23
24
|
export const localStorageThreadStore: ThreadIdStore = {
|
|
@@ -210,6 +211,18 @@ export interface CreateUiResourceReaderOptions {
|
|
|
210
211
|
apiBaseUrl: string;
|
|
211
212
|
/** The thread whose persisted locators this reader may resolve. */
|
|
212
213
|
threadId: string;
|
|
214
|
+
/**
|
|
215
|
+
* The pod base (or full invoke URL — same normalization as the invoke
|
|
216
|
+
* transport). When set, the reader tries the POD door first
|
|
217
|
+
* (`GET <base>/agent/ui-resource`, guuey#209 C1): the pod is the only
|
|
218
|
+
* party that can vouch for a locator whose turn is still streaming —
|
|
219
|
+
* persisted `kind:'card'` rows land at turn COMPLETION, so the platform
|
|
220
|
+
* door 404s mid-turn by construction. Completed turns 404 on the pod
|
|
221
|
+
* (past its grace window) and resolve on the platform door instead: one
|
|
222
|
+
* authority per lifecycle phase, and this reader tries both in that
|
|
223
|
+
* order. Absent → platform door only (pre-#209 behavior).
|
|
224
|
+
*/
|
|
225
|
+
endpointUrl?: string | null;
|
|
213
226
|
/** Signed-in bearer — wins over the guest secret (same rule as the transport). */
|
|
214
227
|
getAccessToken?: (opts?: { forceRefresh?: boolean }) => Promise<string | null>;
|
|
215
228
|
/** Caller-owned anonymous guest secret (widget / guest chat). */
|
|
@@ -218,43 +231,68 @@ export interface CreateUiResourceReaderOptions {
|
|
|
218
231
|
fetchImpl?: typeof fetch;
|
|
219
232
|
}
|
|
220
233
|
|
|
234
|
+
/** `<pod base>/agent/ui-resource` from whatever endpoint shape the surface holds. */
|
|
235
|
+
function toUiResourceUrl(endpointUrl: string): string {
|
|
236
|
+
return toInvokeUrl(endpointUrl).replace(/\/agent\/invoke$/, "/agent/ui-resource");
|
|
237
|
+
}
|
|
238
|
+
|
|
221
239
|
/**
|
|
222
|
-
* Build a `UiResourceReader` over guuey's authenticated resources/read
|
|
223
|
-
* (guuey#
|
|
240
|
+
* Build a `UiResourceReader` over guuey's authenticated resources/read
|
|
241
|
+
* doors — the pod door for LIVE turns (guuey#209 C1:
|
|
242
|
+
* `GET <pod>/agent/ui-resource?uri=…`, when {@link CreateUiResourceReaderOptions.endpointUrl}
|
|
243
|
+
* is set) and the platform proxy for persisted locators (guuey#122 Gap 1:
|
|
244
|
+
* `GET /v1/threads/:threadId/ui-resource?uri=…`). Both doors answer the
|
|
245
|
+
* same body and speak the same identity (bearer wins, guest header
|
|
246
|
+
* otherwise — the pod's `resolveIdentity` and the proxy's identity chain
|
|
247
|
+
* accept the identical carriers), so one parse serves both.
|
|
224
248
|
*
|
|
225
249
|
* This is `@guuey/mcp-apps-host`'s `createMcpUiResourceReader` assembly over
|
|
226
250
|
* a guuey-platform transport (guuey#127) — channel resolution and payload
|
|
227
251
|
* narrowing live in the host package; only the transport is guuey-shaped.
|
|
228
|
-
* The
|
|
229
|
-
* families as the history read),
|
|
230
|
-
* scope guard, and the
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
252
|
+
* The doors own EVERYTHING trust-shaped: caller identity (the same three
|
|
253
|
+
* families as the history read), tenancy (the pod's live-card ledger; the
|
|
254
|
+
* proxy's thread-ownership + locator-to-thread scope guard), and the
|
|
255
|
+
* per-user federation mint. This transport only carries the surface's
|
|
256
|
+
* existing credential and maps EVERY non-OK — 401/403/404/502 alike — to
|
|
257
|
+
* "try the next door", and a miss on the last door to `undefined`: deny is
|
|
258
|
+
* byte-identical to a miss, and a miss renders the host's placeholder,
|
|
259
|
+
* never an error surface.
|
|
234
260
|
*/
|
|
235
261
|
export function createUiResourceReader(
|
|
236
262
|
options: CreateUiResourceReaderOptions,
|
|
237
263
|
): (resourceUri: string) => Promise<ResolvedViewMount | undefined> {
|
|
238
264
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
239
|
-
|
|
265
|
+
|
|
266
|
+
/** One door: fetch + the history adapter's 401-forceRefresh recovery + parse. */
|
|
267
|
+
const readDoor = async (requestUrl: string): Promise<McpResourceReadResult | undefined> => {
|
|
268
|
+
// Exactly ONE identity carrier per read, the invoke transport's rule
|
|
269
|
+
// (`streamInvokeOnce`): bearer → guest header → else cookie credentials,
|
|
270
|
+
// which round-trip the HttpOnly `guuey_guest` cookie the pod mints for
|
|
271
|
+
// anonymous browser callers. Without the third arm a cookie-mode guest
|
|
272
|
+
// sent an identity-less read and every locator rendered as expired
|
|
273
|
+
// (guuey#221). Never two at once: a request carrying either header does
|
|
274
|
+
// NOT also send cookies.
|
|
240
275
|
const headers: Record<string, string> = {};
|
|
276
|
+
const init: RequestInit = { headers };
|
|
241
277
|
const token = options.getAccessToken ? await options.getAccessToken() : null;
|
|
242
278
|
const guest = sendableGuestSecret(options.guestSecret);
|
|
243
279
|
if (token) {
|
|
244
280
|
headers["authorization"] = `Bearer ${token}`;
|
|
245
281
|
} else if (guest) {
|
|
246
282
|
headers[GUEST_HEADER] = guest;
|
|
283
|
+
} else {
|
|
284
|
+
init.credentials = "include";
|
|
247
285
|
}
|
|
248
|
-
const requestUrl = `${options.apiBaseUrl}/threads/${encodeURIComponent(options.threadId)}/ui-resource?uri=${encodeURIComponent(resourceUri)}`;
|
|
249
286
|
let res: Response;
|
|
250
287
|
try {
|
|
251
|
-
res = await fetchImpl(requestUrl,
|
|
288
|
+
res = await fetchImpl(requestUrl, init);
|
|
252
289
|
} catch {
|
|
253
|
-
return undefined; // transport failure == miss
|
|
290
|
+
return undefined; // transport failure == miss (the next door may still answer)
|
|
254
291
|
}
|
|
255
292
|
// One forceRefresh retry on 401 with a bearer in play — the same
|
|
256
293
|
// expired-but-refreshable recovery the history adapter performs;
|
|
257
|
-
// without it a stale token degrades to a permanent placeholder.
|
|
294
|
+
// without it a stale token degrades to a permanent placeholder. The
|
|
295
|
+
// retry carries the fresh bearer and nothing else (same one-carrier rule).
|
|
258
296
|
if (res.status === 401 && options.getAccessToken) {
|
|
259
297
|
const fresh = await options.getAccessToken({ forceRefresh: true }).catch(() => null);
|
|
260
298
|
if (fresh) {
|
|
@@ -274,8 +312,8 @@ export function createUiResourceReader(
|
|
|
274
312
|
} catch {
|
|
275
313
|
return undefined;
|
|
276
314
|
}
|
|
277
|
-
//
|
|
278
|
-
// silently a miss —
|
|
315
|
+
// Both doors pass the blob arm through (a blob-only resource is not
|
|
316
|
+
// silently a miss — the route contract); mirror that here.
|
|
279
317
|
if (typeof body.uri !== "string") return undefined;
|
|
280
318
|
if (typeof body.text !== "string" && typeof body.blob !== "string") return undefined;
|
|
281
319
|
return {
|
|
@@ -285,6 +323,18 @@ export function createUiResourceReader(
|
|
|
285
323
|
...(typeof body.blob === "string" ? { blob: body.blob } : {}),
|
|
286
324
|
};
|
|
287
325
|
};
|
|
326
|
+
|
|
327
|
+
const podUrl = options.endpointUrl ? toUiResourceUrl(options.endpointUrl) : null;
|
|
328
|
+
const readResource = async (resourceUri: string): Promise<McpResourceReadResult | undefined> => {
|
|
329
|
+
const query = `?uri=${encodeURIComponent(resourceUri)}`;
|
|
330
|
+
if (podUrl !== null) {
|
|
331
|
+
const live = await readDoor(`${podUrl}${query}`);
|
|
332
|
+
if (live !== undefined) return live;
|
|
333
|
+
}
|
|
334
|
+
return readDoor(
|
|
335
|
+
`${options.apiBaseUrl}/threads/${encodeURIComponent(options.threadId)}/ui-resource${query}`,
|
|
336
|
+
);
|
|
337
|
+
};
|
|
288
338
|
return createMcpUiResourceReader({ readResource });
|
|
289
339
|
}
|
|
290
340
|
|