@buildinternet/uploads 0.42.0 → 0.42.2

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.
@@ -2,12 +2,20 @@
2
2
  * Install thin user-global hook manifests for Grok and Cursor.
3
3
  *
4
4
  * Claude and Codex ship the same PreToolUse hook via their plugins
5
- * (`hooks/hooks.json` → `uploads hook pre-pr-screenshot`). Do not also write
5
+ * (`hooks/hooks.json` → HOOK_COMMAND). Do not also write
6
6
  * ~/.codex/hooks.json here, or the reminder would fire twice.
7
7
  *
8
8
  * Idempotent: skip when our command string is already present.
9
+ * An older bare `uploads hook …` entry is upgraded in place.
9
10
  */
10
- export declare const HOOK_COMMAND = "uploads hook pre-pr-screenshot";
11
+ /** What the harness actually runs once `uploads` is on PATH. */
12
+ export declare const HOOK_INVOCATION = "uploads hook pre-pr-screenshot";
13
+ /**
14
+ * Fail-open shell used in every harness manifest.
15
+ * A missing `uploads` binary is a silent no-op (exit 0, no stderr) so plugin
16
+ * users without the CLI do not get a hook-failure annotation on every shell tool.
17
+ */
18
+ export declare const HOOK_COMMAND = "if command -v uploads >/dev/null 2>&1; then uploads hook pre-pr-screenshot; fi";
11
19
  export type HookWriteResult = {
12
20
  path: string;
13
21
  action: "wrote" | "merged" | "skipped" | "would-write" | "would-merge";
@@ -2,15 +2,23 @@
2
2
  * Install thin user-global hook manifests for Grok and Cursor.
3
3
  *
4
4
  * Claude and Codex ship the same PreToolUse hook via their plugins
5
- * (`hooks/hooks.json` → `uploads hook pre-pr-screenshot`). Do not also write
5
+ * (`hooks/hooks.json` → HOOK_COMMAND). Do not also write
6
6
  * ~/.codex/hooks.json here, or the reminder would fire twice.
7
7
  *
8
8
  * Idempotent: skip when our command string is already present.
9
+ * An older bare `uploads hook …` entry is upgraded in place.
9
10
  */
10
11
  import fs from "node:fs";
11
12
  import os from "node:os";
12
13
  import path from "node:path";
13
- export const HOOK_COMMAND = "uploads hook pre-pr-screenshot";
14
+ /** What the harness actually runs once `uploads` is on PATH. */
15
+ export const HOOK_INVOCATION = "uploads hook pre-pr-screenshot";
16
+ /**
17
+ * Fail-open shell used in every harness manifest.
18
+ * A missing `uploads` binary is a silent no-op (exit 0, no stderr) so plugin
19
+ * users without the CLI do not get a hook-failure annotation on every shell tool.
20
+ */
21
+ export const HOOK_COMMAND = `if command -v uploads >/dev/null 2>&1; then ${HOOK_INVOCATION}; fi`;
14
22
  const TIMEOUT_SEC = 15;
15
23
  const GROK_PAYLOAD = {
16
24
  description: "Advisory reminder to stage screenshots on uploads.sh before opening a PR that touches UI files.",
@@ -38,6 +46,16 @@ function containsCommand(filePath) {
38
46
  return false;
39
47
  }
40
48
  }
49
+ function hookCommandOf(entry) {
50
+ if (!entry || typeof entry !== "object")
51
+ return undefined;
52
+ const command = entry.command;
53
+ return typeof command === "string" ? command : undefined;
54
+ }
55
+ function isOurHookEntry(entry) {
56
+ const command = hookCommandOf(entry);
57
+ return Boolean(command?.includes(HOOK_INVOCATION));
58
+ }
41
59
  function writeJson(filePath, value) {
42
60
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
43
61
  fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
@@ -79,7 +97,17 @@ function mergeCursor(home, dryRun) {
79
97
  ? { ...existing.hooks }
80
98
  : {};
81
99
  const list = Array.isArray(hooks.beforeShellExecution) ? [...hooks.beforeShellExecution] : [];
82
- list.push({ command: HOOK_COMMAND, timeout: TIMEOUT_SEC });
100
+ const ours = list.findIndex(isOurHookEntry);
101
+ const nextEntry = { command: HOOK_COMMAND, timeout: TIMEOUT_SEC };
102
+ if (ours >= 0) {
103
+ if (hookCommandOf(list[ours]) === HOOK_COMMAND) {
104
+ return { path: filePath, action: "skipped" };
105
+ }
106
+ list[ours] = { ...list[ours], ...nextEntry };
107
+ }
108
+ else {
109
+ list.push(nextEntry);
110
+ }
83
111
  hooks.beforeShellExecution = list;
84
112
  existing.hooks = hooks;
85
113
  if (existing.version === undefined)
@@ -0,0 +1,37 @@
1
+ /**
2
+ * JSON Schema for MCP tool `structuredContent`. The SDK validates successful
3
+ * results against the advertised `outputSchema`, so every property a handler
4
+ * actually returns must be listed. Roots are always `type: object` — a
5
+ * oneOf/anyOf root is treated as non-object by the 2025-era codec and wraps
6
+ * the result as `{ result: … }`.
7
+ */
8
+ export type JsonSchema = Record<string, unknown>;
9
+ /** Hosted + stdio `comment` / `put.comment` — all PostCommentResult variants. */
10
+ export declare const commentResultSchema: JsonSchema;
11
+ /** Single-file put (flat) or multi-file `{ uploads, failures }`, plus optional comment/promote extras. */
12
+ export declare const putResultSchema: JsonSchema;
13
+ export declare const listResultSchema: JsonSchema;
14
+ export declare const deleteResultSchema: JsonSchema;
15
+ export declare const metadataResultSchema: JsonSchema;
16
+ export declare const findFilesResultSchema: JsonSchema;
17
+ /** `meta keys` or `meta values <key>`. */
18
+ export declare const metadataFacetsResultSchema: JsonSchema;
19
+ export declare const repoLinkStatusResultSchema: JsonSchema;
20
+ export declare const usageResultSchema: JsonSchema;
21
+ export declare const reconcileResultSchema: JsonSchema;
22
+ export declare const purgeExpiredResultSchema: JsonSchema;
23
+ export declare const healthResultSchema: JsonSchema;
24
+ export declare const promoteToolResultSchema: JsonSchema;
25
+ export declare const galleryResultSchema: JsonSchema;
26
+ export declare const galleryFindResultSchema: JsonSchema;
27
+ /** Hosted catalog — every tool must have an entry. */
28
+ export declare const hostedOutputSchemas: Record<string, JsonSchema>;
29
+ /** Shared-shape stdio tools. Hosted-only tools (`promote`, `repo_link_status`) omitted. */
30
+ export declare const stdioOutputSchemas: Record<string, JsonSchema>;
31
+ export declare function withOutputSchemas<T extends {
32
+ name: string;
33
+ }>(tools: T[], schemas: Record<string, JsonSchema>, opts: {
34
+ required: boolean;
35
+ }): Array<T & {
36
+ outputSchema?: JsonSchema;
37
+ }>;
@@ -0,0 +1,338 @@
1
+ function objectSchema(properties, required = []) {
2
+ return {
3
+ type: "object",
4
+ properties,
5
+ ...(required.length > 0 ? { required } : {}),
6
+ additionalProperties: false,
7
+ };
8
+ }
9
+ const stringMap = {
10
+ type: "object",
11
+ additionalProperties: { type: "string" },
12
+ };
13
+ const nullableString = { type: ["string", "null"] };
14
+ const nullableNumber = { type: ["number", "null"] };
15
+ /** Hosted + stdio `comment` / `put.comment` — all PostCommentResult variants. */
16
+ export const commentResultSchema = objectSchema({
17
+ posted: { type: "boolean" },
18
+ reason: {
19
+ type: "string",
20
+ enum: [
21
+ "app_unconfigured",
22
+ "not_installed",
23
+ "not_authorized",
24
+ "actor_not_authorized",
25
+ "unavailable",
26
+ "forbidden",
27
+ ],
28
+ },
29
+ message: { type: "string" },
30
+ fixUrl: { type: "string" },
31
+ required: { type: "array", items: { type: "string" } },
32
+ action: { type: "string", enum: ["skipped", "created", "updated"] },
33
+ count: { type: "number" },
34
+ commentUrl: { type: "string" },
35
+ }, ["posted"]);
36
+ const promoteResultSchema = objectSchema({
37
+ promoted: { type: "array", items: { type: "string" } },
38
+ skipped: {
39
+ type: "array",
40
+ items: objectSchema({ key: { type: "string" }, reason: { type: "string" } }, [
41
+ "key",
42
+ "reason",
43
+ ]),
44
+ },
45
+ }, ["promoted", "skipped"]);
46
+ const putObjectFields = {
47
+ key: { type: "string" },
48
+ url: nullableString,
49
+ embedUrl: nullableString,
50
+ size: { type: "number" },
51
+ contentType: { type: "string" },
52
+ replaced: { type: "boolean" },
53
+ markdown: { type: "string" },
54
+ provenance: stringMap,
55
+ metadata: stringMap,
56
+ visibility: { type: "string" },
57
+ };
58
+ /** Hosted `PostCommentResult` or stdio `AttachmentsCommentResult` (`via`/`action`). */
59
+ const putCommentSchema = objectSchema({
60
+ posted: { type: "boolean" },
61
+ reason: {
62
+ type: "string",
63
+ enum: [
64
+ "app_unconfigured",
65
+ "not_installed",
66
+ "not_authorized",
67
+ "actor_not_authorized",
68
+ "unavailable",
69
+ "forbidden",
70
+ ],
71
+ },
72
+ message: { type: "string" },
73
+ fixUrl: { type: "string" },
74
+ required: { type: "array", items: { type: "string" } },
75
+ action: { type: "string", enum: ["skipped", "created", "updated"] },
76
+ count: { type: "number" },
77
+ commentUrl: { type: "string" },
78
+ via: { type: "string", enum: ["bot", "gh"] },
79
+ });
80
+ const putExtras = {
81
+ comment: putCommentSchema,
82
+ commentError: { type: "string" },
83
+ promotion: promoteResultSchema,
84
+ promoteError: { type: "string" },
85
+ };
86
+ const putFailure = objectSchema({
87
+ file: { type: "string" },
88
+ error: objectSchema({
89
+ message: { type: "string" },
90
+ code: { type: "string" },
91
+ status: { type: "number" },
92
+ }, ["message"]),
93
+ }, ["file", "error"]);
94
+ /** Single-file put (flat) or multi-file `{ uploads, failures }`, plus optional comment/promote extras. */
95
+ export const putResultSchema = objectSchema({
96
+ workspace: { type: "string" },
97
+ ...putObjectFields,
98
+ file: { type: "string" },
99
+ uploads: {
100
+ type: "array",
101
+ items: {
102
+ type: "object",
103
+ properties: { file: { type: "string" }, ...putObjectFields },
104
+ additionalProperties: true,
105
+ },
106
+ },
107
+ failures: { type: "array", items: putFailure },
108
+ ...putExtras,
109
+ // stdio put adds client-side optimize/frame provenance and dry-run flags.
110
+ optimize: { type: "object", additionalProperties: true },
111
+ frame: { type: "object", additionalProperties: true },
112
+ dryRun: { type: "boolean" },
113
+ hint: { type: "string" },
114
+ });
115
+ export const listResultSchema = objectSchema({
116
+ items: {
117
+ type: "array",
118
+ items: objectSchema({
119
+ key: { type: "string" },
120
+ url: nullableString,
121
+ embedUrl: nullableString,
122
+ size: { type: "number" },
123
+ contentType: { type: "string" },
124
+ uploaded: { type: "string" },
125
+ visibility: { type: "string" },
126
+ pageUrl: { type: "string" },
127
+ }),
128
+ },
129
+ cursor: nullableString,
130
+ prefixes: { type: "array", items: { type: "string" } },
131
+ });
132
+ export const deleteResultSchema = objectSchema({
133
+ key: { type: "string" },
134
+ deleted: { type: "boolean" },
135
+ dryRun: { type: "boolean" },
136
+ }, ["key"]);
137
+ export const metadataResultSchema = objectSchema({ metadata: stringMap }, ["metadata"]);
138
+ export const findFilesResultSchema = objectSchema({
139
+ items: {
140
+ type: "array",
141
+ items: objectSchema({
142
+ key: { type: "string" },
143
+ url: nullableString,
144
+ metadata: stringMap,
145
+ }, ["key"]),
146
+ },
147
+ cursor: nullableString,
148
+ truncated: { type: "boolean" },
149
+ }, ["items"]);
150
+ /** `meta keys` or `meta values <key>`. */
151
+ export const metadataFacetsResultSchema = objectSchema({
152
+ keys: {
153
+ type: "array",
154
+ items: objectSchema({
155
+ key: { type: "string" },
156
+ count: { type: "number" },
157
+ distinctValues: { type: "number" },
158
+ }, ["key", "count", "distinctValues"]),
159
+ },
160
+ truncated: { type: "boolean" },
161
+ key: { type: "string" },
162
+ values: {
163
+ type: "array",
164
+ items: objectSchema({ value: { type: "string" }, count: { type: "number" } }, [
165
+ "value",
166
+ "count",
167
+ ]),
168
+ },
169
+ });
170
+ export const repoLinkStatusResultSchema = objectSchema({ binding: { type: "string", enum: ["self", "other", "none"] } }, ["binding"]);
171
+ export const usageResultSchema = objectSchema({
172
+ workspace: { type: "string" },
173
+ bytes: { type: "number" },
174
+ objects: { type: "number" },
175
+ uploadsInPeriod: { type: "number" },
176
+ periodStart: { type: "string" },
177
+ updatedAt: { type: "string" },
178
+ maxStorageBytes: { type: "number" },
179
+ storageRemainingBytes: { type: "number" },
180
+ maxUploadsPerPeriod: { type: "number" },
181
+ uploadsRemaining: { type: "number" },
182
+ });
183
+ export const reconcileResultSchema = objectSchema({
184
+ workspace: { type: "string" },
185
+ bytes: { type: "number" },
186
+ objects: { type: "number" },
187
+ previous: objectSchema({ bytes: { type: "number" }, objects: { type: "number" } }, [
188
+ "bytes",
189
+ "objects",
190
+ ]),
191
+ changed: { type: "boolean" },
192
+ usage: usageResultSchema,
193
+ unprefixedBucket: { type: "boolean" },
194
+ });
195
+ export const purgeExpiredResultSchema = objectSchema({
196
+ skipped: { type: "boolean" },
197
+ reason: { type: "string" },
198
+ workspace: { type: "string" },
199
+ retentionDays: { type: "number" },
200
+ cutoff: { type: "string" },
201
+ deleted: { type: "number" },
202
+ freedBytes: { type: "number" },
203
+ keys: { type: "array", items: { type: "string" } },
204
+ keysTruncated: { type: "boolean" },
205
+ reconcile: reconcileResultSchema,
206
+ });
207
+ export const healthResultSchema = objectSchema({
208
+ ok: { type: "boolean" },
209
+ apiUrl: { type: "string" },
210
+ });
211
+ export const promoteToolResultSchema = objectSchema({
212
+ promotion: promoteResultSchema,
213
+ comment: commentResultSchema,
214
+ commentError: { type: "string" },
215
+ }, ["promotion"]);
216
+ const galleryItemSchema = objectSchema({
217
+ id: { type: "string" },
218
+ objectKey: { type: "string" },
219
+ filename: { type: "string" },
220
+ position: { type: "number" },
221
+ caption: nullableString,
222
+ altText: nullableString,
223
+ createdAt: { type: "string" },
224
+ status: { type: "string", enum: ["available", "missing", "withheld"] },
225
+ url: nullableString,
226
+ embedUrl: nullableString,
227
+ pageUrl: { type: "string" },
228
+ contentType: nullableString,
229
+ size: nullableNumber,
230
+ uploaded: nullableString,
231
+ modified: nullableString,
232
+ posterUrl: { type: "string" },
233
+ videoDimensions: {
234
+ type: "object",
235
+ additionalProperties: true,
236
+ },
237
+ });
238
+ const galleryReferenceSchema = objectSchema({
239
+ id: { type: "string" },
240
+ provider: { type: "string" },
241
+ resourceType: { type: "string" },
242
+ coordinate: { type: "string" },
243
+ canonicalUrl: nullableString,
244
+ createdAt: { type: "string" },
245
+ title: { type: "string" },
246
+ kind: { type: "string", enum: ["pull", "issue"] },
247
+ });
248
+ export const galleryResultSchema = objectSchema({
249
+ id: { type: "string" },
250
+ url: { type: "string" },
251
+ workspace: { type: "string" },
252
+ title: { type: "string" },
253
+ description: nullableString,
254
+ visibility: { type: "string" },
255
+ coverItemId: nullableString,
256
+ version: { type: "number" },
257
+ createdAt: { type: "string" },
258
+ updatedAt: { type: "string" },
259
+ items: { type: "array", items: galleryItemSchema },
260
+ itemCount: { type: "number" },
261
+ references: { type: "array", items: galleryReferenceSchema },
262
+ });
263
+ export const galleryFindResultSchema = objectSchema({
264
+ galleries: { type: "array", items: galleryResultSchema },
265
+ nextCursor: nullableString,
266
+ });
267
+ /** Hosted catalog — every tool must have an entry. */
268
+ export const hostedOutputSchemas = {
269
+ gallery_create: galleryResultSchema,
270
+ gallery_get: galleryResultSchema,
271
+ gallery_add: galleryItemSchema,
272
+ gallery_link: galleryReferenceSchema,
273
+ gallery_find_by_reference: galleryFindResultSchema,
274
+ put: putResultSchema,
275
+ list: listResultSchema,
276
+ delete: deleteResultSchema,
277
+ comment: commentResultSchema,
278
+ promote: promoteToolResultSchema,
279
+ get_metadata: metadataResultSchema,
280
+ set_metadata: metadataResultSchema,
281
+ find_files: findFilesResultSchema,
282
+ list_metadata_keys: metadataFacetsResultSchema,
283
+ repo_link_status: repoLinkStatusResultSchema,
284
+ usage: usageResultSchema,
285
+ reconcile: reconcileResultSchema,
286
+ purge_expired: purgeExpiredResultSchema,
287
+ health: healthResultSchema,
288
+ };
289
+ /** Shared-shape stdio tools. Hosted-only tools (`promote`, `repo_link_status`) omitted. */
290
+ export const stdioOutputSchemas = {
291
+ gallery_create: galleryResultSchema,
292
+ gallery_get: galleryResultSchema,
293
+ gallery_add: galleryItemSchema,
294
+ gallery_link: galleryReferenceSchema,
295
+ gallery_find_by_reference: galleryFindResultSchema,
296
+ put: putResultSchema,
297
+ list: listResultSchema,
298
+ delete: deleteResultSchema,
299
+ comment: objectSchema({
300
+ posted: { type: "boolean" },
301
+ reason: { type: "string" },
302
+ message: { type: "string" },
303
+ fixUrl: { type: "string" },
304
+ required: { type: "array", items: { type: "string" } },
305
+ action: { type: "string", enum: ["skipped", "created", "updated"] },
306
+ count: { type: "number" },
307
+ commentUrl: { type: "string" },
308
+ repo: { type: "string" },
309
+ kind: { type: "string" },
310
+ num: { type: "number" },
311
+ via: { type: "string", enum: ["bot", "gh"] },
312
+ }),
313
+ get_metadata: metadataResultSchema,
314
+ set_metadata: metadataResultSchema,
315
+ find_files: findFilesResultSchema,
316
+ list_metadata_keys: metadataFacetsResultSchema,
317
+ usage: usageResultSchema,
318
+ reconcile: reconcileResultSchema,
319
+ purge_expired: purgeExpiredResultSchema,
320
+ health: healthResultSchema,
321
+ report: objectSchema({
322
+ ok: { type: "boolean" },
323
+ id: { type: "string" },
324
+ hasAttachment: { type: "boolean" },
325
+ }, ["ok"]),
326
+ };
327
+ export function withOutputSchemas(tools, schemas, opts) {
328
+ return tools.map((tool) => {
329
+ const outputSchema = schemas[tool.name];
330
+ if (!outputSchema) {
331
+ if (opts.required) {
332
+ throw new Error(`missing output schema for MCP tool ${tool.name}`);
333
+ }
334
+ return tool;
335
+ }
336
+ return { ...tool, outputSchema };
337
+ });
338
+ }
@@ -22,11 +22,63 @@ export { appProp, canonicalMetaFromArgs, METADATA_DESCRIPTION, metadataArgWithCa
22
22
  export { ToolBatchError, batchFailureMessage } from "./batch-error.js";
23
23
  export { mapBounded } from "../async.js";
24
24
  export { McpServer, type jsonSchemaValidator };
25
+ export { commentResultSchema, deleteResultSchema, findFilesResultSchema, galleryFindResultSchema, galleryResultSchema, healthResultSchema, hostedOutputSchemas, listResultSchema, metadataFacetsResultSchema, metadataResultSchema, promoteToolResultSchema, purgeExpiredResultSchema, putResultSchema, reconcileResultSchema, repoLinkStatusResultSchema, stdioOutputSchemas, usageResultSchema, withOutputSchemas, } from "./output-schemas.js";
26
+ /** MCP tool safety hints. Required so tools/list advertises them for review. */
27
+ export interface McpToolAnnotations {
28
+ readOnlyHint: boolean;
29
+ destructiveHint: boolean;
30
+ openWorldHint: boolean;
31
+ }
32
+ /** Lookup / list / health. Does not change workspace or public state. */
33
+ export declare const mcpRead: McpToolAnnotations;
34
+ /** Creates or updates a public object, gallery, or comment without deleting. */
35
+ export declare const mcpWritePublic: McpToolAnnotations;
36
+ /** Deletes or overwrites a public object or a public GitHub comment. */
37
+ export declare const mcpDestroyPublic: McpToolAnnotations;
38
+ /** Mutates first-party / internal state only (ledger, reports). */
39
+ export declare const mcpWriteInternal: McpToolAnnotations;
40
+ /**
41
+ * Per-tool auth policy for ChatGPT / Codex plugin review. Advertised on
42
+ * tools/list as `_meta.securitySchemes` (the SDK has no first-class field).
43
+ */
44
+ export type McpSecurityScheme = {
45
+ type: "noauth";
46
+ } | {
47
+ type: "oauth2";
48
+ scopes: string[];
49
+ };
50
+ export declare const mcpOAuthRead: McpSecurityScheme[];
51
+ export declare const mcpOAuthWrite: McpSecurityScheme[];
52
+ export declare const mcpOAuthDelete: McpSecurityScheme[];
53
+ /** Authenticated, no particular file scope (hosted `health`). */
54
+ export declare const mcpOAuthAny: McpSecurityScheme[];
55
+ /** Callable without a token (stdio `health`). */
56
+ export declare const mcpNoAuth: McpSecurityScheme[];
57
+ /**
58
+ * Thrown when a presented token is missing a required scope. wrapHandler
59
+ * turns this into a tool error that carries `_meta["mcp/www_authenticate"]`
60
+ * so ChatGPT can prompt a re-consent.
61
+ */
62
+ export declare class McpAuthError extends Error {
63
+ readonly challenge: string;
64
+ constructor(message: string, challenge: string);
65
+ }
66
+ /** Build an insufficient_scope challenge pointing at this resource's metadata. */
67
+ export declare function insufficientScopeError(resourceMetadataUrl: string, scope: string): McpAuthError;
25
68
  export interface McpTool {
26
69
  name: string;
27
70
  description: string;
71
+ /** Short label for tools/list. Falls back to `name` when omitted. */
72
+ title?: string;
73
+ annotations: McpToolAnnotations;
74
+ securitySchemes: McpSecurityScheme[];
28
75
  /** Hand-written JSON Schema for the tool's arguments. */
29
76
  inputSchema: Record<string, unknown>;
77
+ /**
78
+ * Hand-written JSON Schema for successful `structuredContent`. Required
79
+ * whenever the handler returns structured data (OpenAI Scan Tools).
80
+ */
81
+ outputSchema?: Record<string, unknown>;
30
82
  handler: (args: Record<string, unknown>) => Promise<unknown>;
31
83
  }
32
84
  export declare function createMcpServer(opts: {
@@ -25,6 +25,58 @@ export { appProp, canonicalMetaFromArgs, METADATA_DESCRIPTION, metadataArgWithCa
25
25
  export { ToolBatchError, batchFailureMessage } from "./batch-error.js";
26
26
  export { mapBounded } from "../async.js";
27
27
  export { McpServer };
28
+ export { commentResultSchema, deleteResultSchema, findFilesResultSchema, galleryFindResultSchema, galleryResultSchema, healthResultSchema, hostedOutputSchemas, listResultSchema, metadataFacetsResultSchema, metadataResultSchema, promoteToolResultSchema, purgeExpiredResultSchema, putResultSchema, reconcileResultSchema, repoLinkStatusResultSchema, stdioOutputSchemas, usageResultSchema, withOutputSchemas, } from "./output-schemas.js";
29
+ /** Lookup / list / health. Does not change workspace or public state. */
30
+ export const mcpRead = {
31
+ readOnlyHint: true,
32
+ destructiveHint: false,
33
+ openWorldHint: false,
34
+ };
35
+ /** Creates or updates a public object, gallery, or comment without deleting. */
36
+ export const mcpWritePublic = {
37
+ readOnlyHint: false,
38
+ destructiveHint: false,
39
+ openWorldHint: true,
40
+ };
41
+ /** Deletes or overwrites a public object or a public GitHub comment. */
42
+ export const mcpDestroyPublic = {
43
+ readOnlyHint: false,
44
+ destructiveHint: true,
45
+ openWorldHint: true,
46
+ };
47
+ /** Mutates first-party / internal state only (ledger, reports). */
48
+ export const mcpWriteInternal = {
49
+ readOnlyHint: false,
50
+ destructiveHint: false,
51
+ openWorldHint: false,
52
+ };
53
+ function oauth(scopes) {
54
+ return [{ type: "oauth2", scopes }];
55
+ }
56
+ export const mcpOAuthRead = oauth(["files:read"]);
57
+ export const mcpOAuthWrite = oauth(["files:write"]);
58
+ export const mcpOAuthDelete = oauth(["files:delete"]);
59
+ /** Authenticated, no particular file scope (hosted `health`). */
60
+ export const mcpOAuthAny = oauth([]);
61
+ /** Callable without a token (stdio `health`). */
62
+ export const mcpNoAuth = [{ type: "noauth" }];
63
+ /**
64
+ * Thrown when a presented token is missing a required scope. wrapHandler
65
+ * turns this into a tool error that carries `_meta["mcp/www_authenticate"]`
66
+ * so ChatGPT can prompt a re-consent.
67
+ */
68
+ export class McpAuthError extends Error {
69
+ challenge;
70
+ constructor(message, challenge) {
71
+ super(message);
72
+ this.name = "McpAuthError";
73
+ this.challenge = challenge;
74
+ }
75
+ }
76
+ /** Build an insufficient_scope challenge pointing at this resource's metadata. */
77
+ export function insufficientScopeError(resourceMetadataUrl, scope) {
78
+ return new McpAuthError(`forbidden: requires ${scope} scope`, `Bearer resource_metadata="${resourceMetadataUrl}", error="insufficient_scope", error_description="This tool requires the ${scope} scope"`);
79
+ }
28
80
  /**
29
81
  * The tool catalog is fixed for the lifetime of a deploy, so a generous
30
82
  * freshness hint is honest. `private` rather than `public` because the list is
@@ -75,6 +127,13 @@ function wrapHandler(tool, apiUrl) {
75
127
  isError: true,
76
128
  };
77
129
  }
130
+ if (err instanceof McpAuthError) {
131
+ return {
132
+ content: [{ type: "text", text: err.message }],
133
+ isError: true,
134
+ _meta: { "mcp/www_authenticate": [err.challenge] },
135
+ };
136
+ }
78
137
  return { content: [{ type: "text", text: toolErrorText(err) }], isError: true };
79
138
  }
80
139
  };
@@ -86,8 +145,14 @@ export function createMcpServer(opts) {
86
145
  });
87
146
  for (const tool of tools) {
88
147
  server.registerTool(tool.name, {
148
+ ...(tool.title ? { title: tool.title } : {}),
89
149
  description: tool.description,
90
150
  inputSchema: fromJsonSchema(tool.inputSchema, validator),
151
+ ...(tool.outputSchema
152
+ ? { outputSchema: fromJsonSchema(tool.outputSchema, validator) }
153
+ : {}),
154
+ annotations: tool.annotations,
155
+ _meta: { securitySchemes: tool.securitySchemes },
91
156
  }, wrapHandler(tool, apiUrl));
92
157
  }
93
158
  return server;