@openparachute/vault 0.7.3-rc.9 → 0.7.4-rc.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/core/src/attachment/bytes-provider.ts +65 -0
- package/core/src/content-range-constants.ts +19 -0
- package/core/src/content-range.test.ts +127 -0
- package/core/src/content-range.ts +105 -8
- package/core/src/core.test.ts +66 -4
- package/core/src/expand.ts +11 -3
- package/core/src/lede.test.ts +96 -0
- package/core/src/mcp-manifest.test.ts +200 -0
- package/core/src/mcp-manifest.ts +736 -0
- package/core/src/mcp.ts +357 -607
- package/core/src/notes.ts +69 -10
- package/core/src/vault-projection.ts +17 -10
- package/package.json +1 -1
- package/src/attachment-bytes.ts +68 -0
- package/src/attachment-tickets.test.ts +126 -1
- package/src/attachment-tickets.ts +77 -1
- package/src/auth-hub-jwt.test.ts +118 -1
- package/src/auth.ts +64 -0
- package/src/config.test.ts +16 -0
- package/src/config.ts +17 -0
- package/src/embedding/select.test.ts +58 -30
- package/src/embedding/select.ts +62 -21
- package/src/embeddings-routes.test.ts +188 -0
- package/src/embeddings-routes.ts +178 -0
- package/src/live-frame-parity.test.ts +21 -0
- package/src/mcp-http.ts +20 -3
- package/src/mcp-tools.ts +15 -3
- package/src/oauth-discovery.ts +31 -0
- package/src/read-attachment.test.ts +436 -0
- package/src/routes.ts +80 -4
- package/src/routing.test.ts +358 -4
- package/src/routing.ts +163 -23
- package/src/scopes.ts +22 -0
- package/src/server.ts +17 -8
- package/src/storage.test.ts +200 -1
- package/src/subscriptions.ts +13 -1
- package/src/transcription-worker.test.ts +151 -0
- package/src/transcription-worker.ts +113 -52
- package/src/vault-embeddings-capability.test.ts +28 -6
- package/src/vault-store-embedding-wiring.test.ts +25 -16
- package/src/vault-store.ts +47 -16
- package/src/vault.test.ts +26 -13
- package/src/ws-server.ts +9 -1
- package/src/ws-subscribe.test.ts +87 -0
- package/src/ws-subscribe.ts +25 -6
- package/web/ui/dist/assets/index-NvwxfZcu.js +61 -0
- package/web/ui/dist/index.html +1 -1
- package/web/ui/dist/assets/index-CJ6NtIwh.js +0 -61
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP surface for the semantic-search (embeddings) opt-in toggle.
|
|
3
|
+
*
|
|
4
|
+
* GET /vault/<name>/.parachute/embeddings — read the persisted setting +
|
|
5
|
+
* the running/effective/env state
|
|
6
|
+
* PUT /vault/<name>/.parachute/embeddings — flip the persisted setting
|
|
7
|
+
*
|
|
8
|
+
* The 0.7.3 fast-follow: 0.7.3 shipped the persisted `embeddings_enabled`
|
|
9
|
+
* config.yaml setting + `resolveEmbeddingsEnabled` (env override → persisted
|
|
10
|
+
* → off), but the ONLY way to flip it was hand-editing config.yaml or setting
|
|
11
|
+
* an env var. This module gives the vault admin SPA a real toggle over that
|
|
12
|
+
* setting.
|
|
13
|
+
*
|
|
14
|
+
* URL note: same reasoning as `mirror-routes.ts` — the admin SPA's static
|
|
15
|
+
* bundle owns `/vault/<name>/admin/*` (vault#252), so the API surface lives
|
|
16
|
+
* under the `.parachute/` module-protocol namespace (sibling to
|
|
17
|
+
* `.parachute/config`, `.parachute/mirror`, `.parachute/usage`).
|
|
18
|
+
*
|
|
19
|
+
* Auth: `vault:admin`, gated upstream in `routing.ts` (this module is the
|
|
20
|
+
* after-auth handler). `embeddings_enabled` is host-global config that affects
|
|
21
|
+
* every vault on the server, but it's reached through the same per-vault admin
|
|
22
|
+
* surface every other settings page uses; the UI copy names the host-wide
|
|
23
|
+
* scope.
|
|
24
|
+
*
|
|
25
|
+
* **Activation is restart-to-apply, not hot.** The embedding provider is
|
|
26
|
+
* resolved ONCE at boot (`getSharedEmbeddingProvider`) and captured into every
|
|
27
|
+
* open store (`Store.embeddingProvider` + `embeddingDisabledReason`, baked at
|
|
28
|
+
* construction) and into the embedding worker (`EmbeddingWorker.provider`, a
|
|
29
|
+
* private readonly field). Re-resolving the provider mid-run would mean
|
|
30
|
+
* mutating readonly fields across the worker AND every already-open store AND
|
|
31
|
+
* resetting a deliberately-once module memo — not clean. So this endpoint
|
|
32
|
+
* PERSISTS the setting and reports `restart_required` when the persisted
|
|
33
|
+
* change hasn't taken effect in the running process yet. Hot-reconfigure is a
|
|
34
|
+
* possible follow-up.
|
|
35
|
+
*
|
|
36
|
+
* **The `EMBEDDINGS_ENABLED` env var still wins.** It's the low-level override
|
|
37
|
+
* (`true`/`1` on, `false`/`0` off, else defer). When it's forcing a value the
|
|
38
|
+
* snapshot carries `env_override` so the UI can flag that the persisted toggle
|
|
39
|
+
* is advisory until the env var is removed — the toggle never lies about
|
|
40
|
+
* what's actually in force.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import { readGlobalConfig, writeGlobalConfig } from "./config.ts";
|
|
44
|
+
import { embeddingsEnabledEnvOverride, resolveEmbeddingsEnabled } from "./embedding/select.ts";
|
|
45
|
+
import { isSharedEmbeddingProviderActive } from "./vault-store.ts";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Approximate size of the bundled ONNX model (`bge-small-en-v1.5`, q8) that
|
|
49
|
+
* lazy-downloads on the FIRST embed after enabling. Surfaced so the UI can
|
|
50
|
+
* warn before the operator flips the switch. (The npm dependency ships ~270MB
|
|
51
|
+
* of runtime, but the model weights fetched on first use are ~34MB.)
|
|
52
|
+
*/
|
|
53
|
+
export const EMBEDDING_MODEL_DOWNLOAD_MB = 34;
|
|
54
|
+
|
|
55
|
+
const CORS = { "Access-Control-Allow-Origin": "*" } as const;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The wire shape the admin SPA reads/writes. Both GET and PUT return this so
|
|
59
|
+
* the SPA reuses one decoder.
|
|
60
|
+
*/
|
|
61
|
+
export interface EmbeddingsSettingsSnapshot {
|
|
62
|
+
/**
|
|
63
|
+
* The persisted `embeddings_enabled` in config.yaml — the value the toggle
|
|
64
|
+
* reflects. `false` when unset (semantic search is opt-in, default off).
|
|
65
|
+
*/
|
|
66
|
+
enabled: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* The `EMBEDDINGS_ENABLED` env override, tri-state: `true` (forced on),
|
|
69
|
+
* `false` (forced off), or `null` (no env var — defers to `enabled`). When
|
|
70
|
+
* non-null the env var wins over the persisted setting.
|
|
71
|
+
*/
|
|
72
|
+
env_override: boolean | null;
|
|
73
|
+
/**
|
|
74
|
+
* Convenience flag: `true` exactly when `env_override` is non-null. The UI
|
|
75
|
+
* shows an advisory banner ("an env var is forcing this") when set.
|
|
76
|
+
*/
|
|
77
|
+
env_forced: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* What a fresh boot would resolve: env override, else persisted, else off.
|
|
80
|
+
* This is the state the running process will land in on next restart.
|
|
81
|
+
*/
|
|
82
|
+
effective: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Whether semantic search is LIVE in the running process right now (the
|
|
85
|
+
* boot-resolved provider is present). Distinct from `effective`: a flip of
|
|
86
|
+
* the persisted setting changes `effective` immediately but not `active`.
|
|
87
|
+
*/
|
|
88
|
+
active: boolean;
|
|
89
|
+
/**
|
|
90
|
+
* `true` when `active !== effective` — the operator's intended state is
|
|
91
|
+
* persisted but the running process hasn't picked it up. Restart to apply.
|
|
92
|
+
*/
|
|
93
|
+
restart_required: boolean;
|
|
94
|
+
/** First-enable model-download size hint (MB) for the UI copy. */
|
|
95
|
+
model_download_mb: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Build the snapshot from the persisted setting + env + running state. Pure
|
|
100
|
+
* over its inputs (env + persisted + active) so it's unit-testable without a
|
|
101
|
+
* live server; the `active` reading is injected (defaults to the real
|
|
102
|
+
* process-shared provider state).
|
|
103
|
+
*/
|
|
104
|
+
export function buildEmbeddingsSnapshot(opts?: {
|
|
105
|
+
env?: NodeJS.ProcessEnv;
|
|
106
|
+
persistedEnabled?: boolean;
|
|
107
|
+
active?: boolean;
|
|
108
|
+
}): EmbeddingsSettingsSnapshot {
|
|
109
|
+
const env = opts?.env ?? process.env;
|
|
110
|
+
const persisted = opts?.persistedEnabled ?? false;
|
|
111
|
+
const active = opts?.active ?? isSharedEmbeddingProviderActive();
|
|
112
|
+
const override = embeddingsEnabledEnvOverride(env);
|
|
113
|
+
const effective = resolveEmbeddingsEnabled(env, persisted);
|
|
114
|
+
return {
|
|
115
|
+
enabled: persisted,
|
|
116
|
+
env_override: override ?? null,
|
|
117
|
+
env_forced: override !== undefined,
|
|
118
|
+
effective,
|
|
119
|
+
active,
|
|
120
|
+
restart_required: active !== effective,
|
|
121
|
+
model_download_mb: EMBEDDING_MODEL_DOWNLOAD_MB,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* `GET /vault/<name>/.parachute/embeddings` — return the current settings
|
|
127
|
+
* snapshot. Always 200 (auth enforced upstream). `activeOverride` is a test
|
|
128
|
+
* seam; production passes nothing and the running provider state is read.
|
|
129
|
+
*/
|
|
130
|
+
export function handleEmbeddingsGet(activeOverride?: boolean): Response {
|
|
131
|
+
const persisted = readGlobalConfig().embeddings_enabled;
|
|
132
|
+
const snapshot = buildEmbeddingsSnapshot({ persistedEnabled: persisted, active: activeOverride });
|
|
133
|
+
return Response.json(snapshot, { headers: CORS });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* `PUT /vault/<name>/.parachute/embeddings` — persist a new
|
|
138
|
+
* `embeddings_enabled` value. Body: `{ "enabled": boolean }`.
|
|
139
|
+
*
|
|
140
|
+
* Reuses the config write path (`writeGlobalConfig` — never hand-rolls YAML),
|
|
141
|
+
* so the serialize logic that already knows how to persist `embeddings_enabled`
|
|
142
|
+
* is the single source of truth. Persists even when the env var is forcing a
|
|
143
|
+
* value (the operator's persisted intent is recorded; the env override still
|
|
144
|
+
* wins for `effective`/`active`, and the snapshot says so). Returns the same
|
|
145
|
+
* shape as GET, reflecting the new persisted value + the unchanged running
|
|
146
|
+
* state (so `restart_required` is honest right after the write).
|
|
147
|
+
*/
|
|
148
|
+
export async function handleEmbeddingsPut(req: Request, activeOverride?: boolean): Promise<Response> {
|
|
149
|
+
let body: unknown;
|
|
150
|
+
try {
|
|
151
|
+
body = await req.json();
|
|
152
|
+
} catch (err) {
|
|
153
|
+
return Response.json(
|
|
154
|
+
{ error: "Invalid JSON body", message: (err as Error).message ?? String(err) },
|
|
155
|
+
{ status: 400 },
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) {
|
|
159
|
+
return Response.json(
|
|
160
|
+
{ error: "Invalid body", field: "enabled", message: "Expected a JSON object { enabled: boolean }." },
|
|
161
|
+
{ status: 400 },
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
const enabled = (body as { enabled?: unknown }).enabled;
|
|
165
|
+
if (typeof enabled !== "boolean") {
|
|
166
|
+
return Response.json(
|
|
167
|
+
{ error: "Invalid body", field: "enabled", message: "`enabled` must be a boolean." },
|
|
168
|
+
{ status: 400 },
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const config = readGlobalConfig();
|
|
173
|
+
config.embeddings_enabled = enabled;
|
|
174
|
+
writeGlobalConfig(config);
|
|
175
|
+
|
|
176
|
+
const snapshot = buildEmbeddingsSnapshot({ persistedEnabled: enabled, active: activeOverride });
|
|
177
|
+
return Response.json(snapshot, { headers: CORS });
|
|
178
|
+
}
|
|
@@ -98,6 +98,18 @@ describe("buildSnapshotFrames — chunking + done flag", () => {
|
|
|
98
98
|
for (const f of frames) expect(new TextEncoder().encode(f).byteLength).toBeLessThan(1_000_000);
|
|
99
99
|
expect(frames.flatMap((f) => JSON.parse(f).notes).length).toBe(8);
|
|
100
100
|
});
|
|
101
|
+
|
|
102
|
+
it("is shape-agnostic — frames a lean NoteIndex entry verbatim", () => {
|
|
103
|
+
// A lean subscription hands `toNoteIndex`-projected entries; the framer
|
|
104
|
+
// serializes them byte-for-byte, no content field re-added.
|
|
105
|
+
const lean = { id: "x", byteSize: 3, preview: "abc", displayTitle: "abc", tags: ["chat"], metadata: {} };
|
|
106
|
+
const frames = buildSnapshotFrames([lean as any]);
|
|
107
|
+
expect(frames.length).toBe(1);
|
|
108
|
+
const f = JSON.parse(frames[0]!);
|
|
109
|
+
expect(f.done).toBe(true);
|
|
110
|
+
expect(f.notes).toEqual([lean]);
|
|
111
|
+
expect(f.notes[0].content).toBeUndefined();
|
|
112
|
+
});
|
|
101
113
|
});
|
|
102
114
|
|
|
103
115
|
describe("parseClientMessage", () => {
|
|
@@ -171,6 +183,15 @@ describe("validateWsSubscribeQuery — same rejects as the SSE route (byte-ident
|
|
|
171
183
|
const v = validateWsSubscribeQuery(new URL("http://x/vault/v/api/subscribe?tag=chat&path_prefix=meetings/"));
|
|
172
184
|
expect("queryOpts" in v).toBe(true);
|
|
173
185
|
});
|
|
186
|
+
it("resolves include_content — default TRUE (full), `false`/`0` → lean, `true`/`1` → full", () => {
|
|
187
|
+
const at = (q: string) =>
|
|
188
|
+
validateWsSubscribeQuery(new URL(`http://x/vault/v/api/subscribe?tag=chat${q}`)) as { includeContent: boolean };
|
|
189
|
+
expect(at("").includeContent).toBe(true); // absent → full (byte-unchanged default)
|
|
190
|
+
expect(at("&include_content=false").includeContent).toBe(false); // opt into lean
|
|
191
|
+
expect(at("&include_content=0").includeContent).toBe(false);
|
|
192
|
+
expect(at("&include_content=true").includeContent).toBe(true);
|
|
193
|
+
expect(at("&include_content=1").includeContent).toBe(true);
|
|
194
|
+
});
|
|
174
195
|
it("shares the SSE route's queryOpts-level guard (cursor/has_links/date filters)", () => {
|
|
175
196
|
// The belt-and-suspenders layer both doors + the SSE route route through:
|
|
176
197
|
// a date filter isn't expressible as a flat URL param (removed 0.6.4), but
|
package/src/mcp-http.ts
CHANGED
|
@@ -180,9 +180,15 @@ export async function handleMcp(
|
|
|
180
180
|
}
|
|
181
181
|
try {
|
|
182
182
|
const result = await tool.execute((args ?? {}) as Record<string, unknown>);
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
// The one wrapper change (attachments-for-agents design, Wave 2):
|
|
184
|
+
// `resultContent`, when a tool defines it, decides the MCP content
|
|
185
|
+
// blocks instead of the universal single-text-block default —
|
|
186
|
+
// `read-attachment`'s image branch is the only current user (needs a
|
|
187
|
+
// REAL {type:"image"} block alongside the row-JSON text block).
|
|
188
|
+
const content = tool.resultContent
|
|
189
|
+
? tool.resultContent(result)
|
|
190
|
+
: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }];
|
|
191
|
+
return { content };
|
|
186
192
|
} catch (err) {
|
|
187
193
|
// vault#555 fix 6 — never re-wrap an already-formed McpError. Passing
|
|
188
194
|
// the SAME instance straight through is strictly correct (no
|
|
@@ -231,6 +237,12 @@ export async function handleMcp(
|
|
|
231
237
|
referencing_tags?: unknown;
|
|
232
238
|
/** Attachment-tickets design (§2c "errors as JIT docs") — a short, imperative next-step, distinct from the older free-form `hint`. */
|
|
233
239
|
how_to?: string;
|
|
240
|
+
/** `read-attachment` (Wave 2) refusals — `image_too_large` / `unsupported_attachment_type` carry the attachment's actual byte size. */
|
|
241
|
+
size?: number;
|
|
242
|
+
/** `read-attachment` `image_too_large` — the 4 MiB cap it exceeded. */
|
|
243
|
+
max_bytes?: number;
|
|
244
|
+
/** `read-attachment` `unsupported_attachment_type` — the mime type that couldn't be read. */
|
|
245
|
+
mime_type?: string;
|
|
234
246
|
};
|
|
235
247
|
// Honest-queries validation errors (vault#550) — `limit`/`offset`/date
|
|
236
248
|
// values that are structurally invalid rather than merely "no
|
|
@@ -411,6 +423,11 @@ export async function handleMcp(
|
|
|
411
423
|
...(e.got !== undefined ? { got: e.got } : {}),
|
|
412
424
|
...(e.extension !== undefined ? { extension: e.extension } : {}),
|
|
413
425
|
...(e.how_to !== undefined ? { how_to: e.how_to } : {}),
|
|
426
|
+
// read-attachment (Wave 2) refusal fields — same forward-when-present
|
|
427
|
+
// discipline as the ticket fields just above.
|
|
428
|
+
...(e.size !== undefined ? { size: e.size } : {}),
|
|
429
|
+
...(e.max_bytes !== undefined ? { max_bytes: e.max_bytes } : {}),
|
|
430
|
+
...(e.mime_type !== undefined ? { mime_type: e.mime_type } : {}),
|
|
414
431
|
});
|
|
415
432
|
}
|
|
416
433
|
return {
|
package/src/mcp-tools.ts
CHANGED
|
@@ -45,6 +45,7 @@ import { looksLikeJwt } from "./hub-jwt.ts";
|
|
|
45
45
|
import { readGlobalConfig, DEFAULT_PORT } from "./config.ts";
|
|
46
46
|
import { getBaseUrl } from "./oauth-discovery.ts";
|
|
47
47
|
import { getSharedAttachmentTicketProvider } from "./attachment-tickets.ts";
|
|
48
|
+
import { createFsAttachmentBytesProvider } from "./attachment-bytes.ts";
|
|
48
49
|
|
|
49
50
|
/**
|
|
50
51
|
* Filter a vault projection to entries an in-scope tag contributes to.
|
|
@@ -112,10 +113,12 @@ export async function getServerInstruction(
|
|
|
112
113
|
description: config?.description ?? null,
|
|
113
114
|
projection,
|
|
114
115
|
coordinates: resolveVaultCoordinates(),
|
|
115
|
-
// Bun always wires an in-process AttachmentTicketProvider
|
|
116
|
+
// Bun always wires an in-process AttachmentTicketProvider AND a fresh
|
|
117
|
+
// fs-backed AttachmentBytesProvider per session (see
|
|
116
118
|
// `generateScopedMcpTools` below) — the connect-time brief can
|
|
117
|
-
// unconditionally teach the ticket tools
|
|
118
|
-
|
|
119
|
+
// unconditionally teach both the ticket tools and read-attachment on
|
|
120
|
+
// this door.
|
|
121
|
+
attachments: { ticketsEnabled: true, readEnabled: true },
|
|
119
122
|
});
|
|
120
123
|
}
|
|
121
124
|
|
|
@@ -273,6 +276,15 @@ export function generateScopedMcpTools(
|
|
|
273
276
|
urlBase: ticketUrlBase,
|
|
274
277
|
...(ticketNoteVisible ? { noteVisible: ticketNoteVisible } : {}),
|
|
275
278
|
},
|
|
279
|
+
// Attachment bytes (Wave 2 model lane): bun always wires a fresh fs
|
|
280
|
+
// provider per session — cheap (stateless), unlike the ticket
|
|
281
|
+
// provider's process-wide shared Map. Same `ticketNoteVisible`
|
|
282
|
+
// tag-scope predicate as the ticket seam above (identical contract:
|
|
283
|
+
// "is the owning note in scope").
|
|
284
|
+
attachmentBytes: {
|
|
285
|
+
provider: createFsAttachmentBytesProvider(vaultName),
|
|
286
|
+
...(ticketNoteVisible ? { noteVisible: ticketNoteVisible } : {}),
|
|
287
|
+
},
|
|
276
288
|
});
|
|
277
289
|
|
|
278
290
|
overrideVaultInfo(tools, vaultName, auth);
|
package/src/oauth-discovery.ts
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
import { getHubOrigin } from "./hub-jwt.ts";
|
|
30
|
+
import { SCOPE_READ, SCOPE_WRITE } from "./scopes.ts";
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* OAuth scopes vault publishes through discovery, RESOURCE-NARROWED to the
|
|
@@ -87,6 +88,36 @@ export function handleProtectedResource(req: Request, vaultName: string): Respon
|
|
|
87
88
|
});
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Protected Resource Metadata (RFC 9728) for the CANONICAL ROOT `/mcp`
|
|
93
|
+
* endpoint (U1). Same document shape as `handleProtectedResource`, with two
|
|
94
|
+
* deliberate differences that follow from the root being vault-AGNOSTIC (the
|
|
95
|
+
* vault is derived from the token, not the URL):
|
|
96
|
+
*
|
|
97
|
+
* - `resource` is the origin-root `<base>/mcp`, not a `/vault/<name>/mcp`.
|
|
98
|
+
* - `scopes_supported` advertises the UN-NARROWED forms `vault:read` /
|
|
99
|
+
* `vault:write` (there's no vault name to narrow to here). A spec-following
|
|
100
|
+
* client reads these and requests the broad forms; the hub's consent picker
|
|
101
|
+
* narrows the grant to a chosen vault at authorization time (that path
|
|
102
|
+
* already exists hub-side), minting a token stamped with a
|
|
103
|
+
* `vault:<name>:<verb>` scope + `aud=vault.<name>` — exactly what the root
|
|
104
|
+
* endpoint derives the target vault from. `admin` is intentionally omitted:
|
|
105
|
+
* the interactive connect flow grants read/write; admin is an operator
|
|
106
|
+
* concern, not something the consent picker offers.
|
|
107
|
+
*
|
|
108
|
+
* Served at the RFC 9728 §3.1 path-insertion location for the root resource:
|
|
109
|
+
* `/.well-known/oauth-protected-resource/mcp`.
|
|
110
|
+
*/
|
|
111
|
+
export function handleRootProtectedResource(req: Request): Response {
|
|
112
|
+
const base = getBaseUrl(req);
|
|
113
|
+
return Response.json({
|
|
114
|
+
resource: `${base}/mcp`,
|
|
115
|
+
authorization_servers: [getHubOrigin()],
|
|
116
|
+
scopes_supported: [SCOPE_READ, SCOPE_WRITE],
|
|
117
|
+
bearer_methods_supported: ["header"],
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
90
121
|
/**
|
|
91
122
|
* OAuth 2.0 Authorization Server Metadata (RFC 8414).
|
|
92
123
|
*
|