@buildinternet/uploads 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/commands.js CHANGED
@@ -5,16 +5,9 @@ import { parseCommandArgs, flagString, flagBool, flagInt, UsageError, } from "./
5
5
  import { resolvePutDefaults, workspaceMismatch, workspaceFromToken, } from "./config.js";
6
6
  import { buildMarkdown } from "./embed.js";
7
7
  import { UploadsError } from "./errors.js";
8
+ import { writeJson, writeStdout } from "./io.js";
8
9
  import { ghAttachmentKey, ghKeyPrefix, attachmentsCommentBody, } from "./github.js";
9
10
  import { resolveRepo, resolveCurrentPullRequest, execRunner, upsertAttachmentsComment, } from "./github-gh.js";
10
- async function writeStdout(text) {
11
- if (!process.stdout.write(text)) {
12
- await new Promise((resolve) => process.stdout.once("drain", resolve));
13
- }
14
- }
15
- async function writeJson(value) {
16
- await writeStdout(JSON.stringify(value, null, 2) + "\n");
17
- }
18
11
  // --- put ---
19
12
  const PUT_HELP = `uploads put <file> [options]
20
13
 
@@ -40,31 +33,30 @@ Examples:
40
33
  uploads --env-file .env put ./shot.png
41
34
  uploads --env-file .env put ./after.png --pr 123 --comment
42
35
  `;
43
- /** Reads --pr/--issue (+ --repo) into a GhTarget; undefined when neither flag is present. */
44
- function ghTargetFromFlags(flags, run) {
45
- const pr = flagInt(flags, "--pr", "--pr");
46
- const issue = flagInt(flags, "--issue", "--issue");
36
+ /**
37
+ * Turns a pr/issue pair (+ optional repo) into a GhTarget; undefined when
38
+ * neither is present. Shared by the CLI flags and the MCP tool arguments.
39
+ */
40
+ export function makeGhTarget(pr, issue, repoArg, run) {
47
41
  if (pr === undefined && issue === undefined)
48
42
  return undefined;
49
43
  if (pr !== undefined && issue !== undefined) {
50
44
  throw new UsageError("--pr and --issue are mutually exclusive");
51
45
  }
52
- const repo = resolveRepo(flagString(flags, "--repo"), run);
46
+ const repo = resolveRepo(repoArg, run);
53
47
  return { repo, kind: pr !== undefined ? "pull" : "issues", num: (pr ?? issue) };
54
48
  }
49
+ /** Reads --pr/--issue (+ --repo) into a GhTarget; undefined when neither flag is present. */
50
+ function ghTargetFromFlags(flags, run) {
51
+ return makeGhTarget(flagInt(flags, "--pr", "--pr"), flagInt(flags, "--issue", "--issue"), flagString(flags, "--repo"), run);
52
+ }
55
53
  /**
56
54
  * List every attachment under the target's prefix and create/update the
57
55
  * managed comment. Throws on gh failure — callers decide whether that is
58
56
  * fatal (`comment` command) or a warning (`put --comment`).
59
57
  */
60
- async function syncAttachmentsComment(ctx, target, run) {
61
- const items = [];
62
- let cursor;
63
- do {
64
- const page = await ctx.client.list({ prefix: ghKeyPrefix(target), cursor });
65
- items.push(...page.items.map(({ key, url }) => ({ key, url })));
66
- cursor = page.cursor ?? undefined;
67
- } while (cursor);
58
+ export async function syncAttachmentsComment(client, target, run) {
59
+ const items = (await client.listAll({ prefix: ghKeyPrefix(target) })).map(({ key, url }) => ({ key, url }));
68
60
  if (items.length === 0)
69
61
  return { action: "skipped", count: 0 };
70
62
  const body = attachmentsCommentBody(items);
@@ -124,7 +116,7 @@ export async function runAttach(ctx, args, help = false, run = execRunner) {
124
116
  let commentError;
125
117
  if (!parsed.flags.has("--no-comment")) {
126
118
  try {
127
- comment = await syncAttachmentsComment(ctx, target, run);
119
+ comment = await syncAttachmentsComment(ctx.client, target, run);
128
120
  }
129
121
  catch (err) {
130
122
  commentError = err instanceof Error ? err.message : String(err);
@@ -230,7 +222,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
230
222
  }
231
223
  if (wantComment && ghTarget) {
232
224
  try {
233
- const sync = await syncAttachmentsComment(ctx, ghTarget, run);
225
+ const sync = await syncAttachmentsComment(ctx.client, ghTarget, run);
234
226
  if (!ctx.quiet && format === "human") {
235
227
  process.stderr.write(`>> attachments comment ${sync.action}\n`);
236
228
  }
@@ -270,13 +262,8 @@ export async function runList(ctx, args, help = false, run = execRunner) {
270
262
  const limit = flagInt(parsed.flags, "--limit", "--limit");
271
263
  const cursor = flagString(parsed.flags, "--cursor");
272
264
  if (flagBool(parsed.flags, "--all")) {
273
- const items = [];
274
- let next = cursor;
275
- do {
276
- const page = await ctx.client.list({ prefix, limit, cursor: next ?? undefined });
277
- items.push(...page.items);
278
- next = page.cursor;
279
- } while (next);
265
+ // --all may start from a caller-provided --cursor and drains from there.
266
+ const items = await ctx.client.listAll({ prefix, limit, cursor });
280
267
  if (ctx.json)
281
268
  await writeJson({ items, cursor: null });
282
269
  else
@@ -347,7 +334,7 @@ export async function runComment(ctx, args, help = false, run = execRunner) {
347
334
  const target = ghTargetFromFlags(parsed.flags, run);
348
335
  if (!target)
349
336
  throw new UsageError("comment requires --pr or --issue");
350
- const result = await syncAttachmentsComment(ctx, target, run);
337
+ const result = await syncAttachmentsComment(ctx.client, target, run);
351
338
  if (ctx.json) {
352
339
  await writeJson({ ...target, ...result });
353
340
  }
@@ -358,6 +345,83 @@ export async function runComment(ctx, args, help = false, run = execRunner) {
358
345
  }
359
346
  return 0;
360
347
  }
348
+ // --- usage / reconcile / purge ---
349
+ const USAGE_HELP = `uploads usage [--workspace <name>]
350
+
351
+ Show workspace storage and monthly upload counters (and limits when set).
352
+
353
+ Examples:
354
+ uploads --env-file .env usage
355
+ uploads usage --json
356
+ `;
357
+ export async function runUsage(ctx, args, help = false) {
358
+ if (help || parseCommandArgs(args).help) {
359
+ process.stderr.write(USAGE_HELP);
360
+ return 0;
361
+ }
362
+ const result = await ctx.client.usage();
363
+ if (ctx.json) {
364
+ await writeJson(result);
365
+ return 0;
366
+ }
367
+ const lines = [
368
+ `workspace: ${result.workspace}`,
369
+ `bytes: ${result.bytes}${result.maxStorageBytes != null ? ` / ${result.maxStorageBytes} (${result.storageRemainingBytes} remaining)` : ""}`,
370
+ `objects: ${result.objects}`,
371
+ `uploads: ${result.uploadsInPeriod} this period (${result.periodStart})${result.maxUploadsPerPeriod != null ? ` / ${result.maxUploadsPerPeriod} (${result.uploadsRemaining} remaining)` : ""}`,
372
+ `updated: ${result.updatedAt}`,
373
+ ];
374
+ await writeStdout(lines.join("\n") + "\n");
375
+ return 0;
376
+ }
377
+ const RECONCILE_HELP = `uploads reconcile [--workspace <name>]
378
+
379
+ Rebuild ledger bytes/objects from storage (source of truth). Preserves the
380
+ monthly upload counter. Requires files:write.
381
+
382
+ Examples:
383
+ uploads --env-file .env reconcile
384
+ `;
385
+ export async function runReconcile(ctx, args, help = false) {
386
+ if (help || parseCommandArgs(args).help) {
387
+ process.stderr.write(RECONCILE_HELP);
388
+ return 0;
389
+ }
390
+ const result = await ctx.client.reconcile();
391
+ if (ctx.json) {
392
+ await writeJson(result);
393
+ return 0;
394
+ }
395
+ await writeStdout(result.changed
396
+ ? `reconciled ${result.workspace}: ${result.previous.bytes}→${result.bytes} bytes, ${result.previous.objects}→${result.objects} objects\n`
397
+ : `reconciled ${result.workspace}: unchanged (${result.bytes} bytes, ${result.objects} objects)\n`);
398
+ return 0;
399
+ }
400
+ const PURGE_HELP = `uploads purge-expired [--workspace <name>]
401
+
402
+ Delete objects older than the workspace retentionDays setting, then reconcile.
403
+ Skips if retention is unset. Requires files:delete.
404
+
405
+ Examples:
406
+ uploads --env-file .env purge-expired
407
+ `;
408
+ export async function runPurgeExpired(ctx, args, help = false) {
409
+ if (help || parseCommandArgs(args).help) {
410
+ process.stderr.write(PURGE_HELP);
411
+ return 0;
412
+ }
413
+ const result = await ctx.client.purgeExpired();
414
+ if (ctx.json) {
415
+ await writeJson(result);
416
+ return 0;
417
+ }
418
+ if ("skipped" in result) {
419
+ await writeStdout(`skipped: ${result.reason}\n`);
420
+ return 0;
421
+ }
422
+ await writeStdout(`purged ${result.deleted} object(s), freed ${result.freedBytes} bytes (retention ${result.retentionDays}d)\n`);
423
+ return 0;
424
+ }
361
425
  // --- health & doctor ---
362
426
  const HEALTH_HELP = `uploads health
363
427
 
@@ -391,23 +455,20 @@ Examples:
391
455
  uploads --env-file .env doctor
392
456
  uploads --workspace acme --env-file .env doctor
393
457
  `;
394
- export async function runDoctor(ctx, args, help = false) {
395
- if (help || parseCommandArgs(args).help) {
396
- process.stderr.write(DOCTOR_HELP);
397
- return 0;
398
- }
399
- const mismatch = workspaceMismatch(ctx.config);
458
+ /** Doctor's health + auth + workspace checks, shared by the CLI and the MCP tool. */
459
+ export async function buildDoctorReport(config, client) {
460
+ const mismatch = workspaceMismatch(config);
400
461
  const hints = [];
401
462
  if (mismatch)
402
463
  hints.push(mismatch);
403
- if (ctx.config.apiUrl.includes("localhost") || ctx.config.apiUrl.includes("127.0.0.1")) {
464
+ if (config.apiUrl.includes("localhost") || config.apiUrl.includes("127.0.0.1")) {
404
465
  hints.push("local API uses dev KV — prod tokens won't work unless minted with --local");
405
466
  }
406
- const health = await ctx.client.health();
467
+ const health = await client.health();
407
468
  let authOk = false;
408
469
  let authError;
409
470
  try {
410
- await ctx.client.list({ limit: 1 });
471
+ await client.list({ limit: 1 });
411
472
  authOk = true;
412
473
  }
413
474
  catch (err) {
@@ -416,35 +477,67 @@ export async function runDoctor(ctx, args, help = false) {
416
477
  hints.push("if this token works on api.uploads.sh, set UPLOADS_API_URL=https://api.uploads.sh");
417
478
  }
418
479
  }
419
- if (!ctx.config.configExists && !ctx.config.token) {
420
- hints.push(`run uploads setup to configure ${ctx.config.configPath}`);
480
+ let usage;
481
+ if (authOk) {
482
+ try {
483
+ const snap = await client.usage();
484
+ usage = {
485
+ ok: true,
486
+ bytes: snap.bytes,
487
+ objects: snap.objects,
488
+ uploadsInPeriod: snap.uploadsInPeriod,
489
+ };
490
+ }
491
+ catch (err) {
492
+ usage = {
493
+ ok: false,
494
+ error: err instanceof UploadsError ? err.message : String(err),
495
+ };
496
+ }
421
497
  }
422
- const report = {
498
+ if (!config.configExists && !config.token) {
499
+ hints.push(`run uploads setup to configure ${config.configPath}`);
500
+ }
501
+ return {
423
502
  ok: health.ok && authOk,
424
- apiUrl: ctx.config.apiUrl,
425
- workspace: ctx.config.workspace,
426
- workspaceSource: ctx.config.workspaceSource,
427
- workspaceFromToken: workspaceFromToken(ctx.config.token),
428
- configPath: ctx.config.configPath,
429
- configExists: ctx.config.configExists,
503
+ apiUrl: config.apiUrl,
504
+ workspace: config.workspace,
505
+ workspaceSource: config.workspaceSource,
506
+ workspaceFromToken: workspaceFromToken(config.token),
507
+ configPath: config.configPath,
508
+ configExists: config.configExists,
430
509
  health,
431
510
  auth: { ok: authOk, error: authError },
511
+ usage,
512
+ warning: mismatch,
432
513
  hints,
433
514
  };
515
+ }
516
+ export async function runDoctor(ctx, args, help = false) {
517
+ if (help || parseCommandArgs(args).help) {
518
+ process.stderr.write(DOCTOR_HELP);
519
+ return 0;
520
+ }
521
+ const report = await buildDoctorReport(ctx.config, ctx.client);
434
522
  if (ctx.json) {
435
523
  await writeJson(report);
436
524
  return report.ok ? 0 : 1;
437
525
  }
438
526
  const lines = [
439
- `config: ${ctx.config.configPath}${ctx.config.configExists ? "" : " (missing)"}`,
440
- `api: ${ctx.config.apiUrl} (${health.ok ? "ok" : "failed"})`,
441
- `workspace: ${ctx.config.workspace}`,
442
- `auth: ${authOk ? "ok" : `failed — ${authError ?? "no token"}`}`,
527
+ `config: ${report.configPath}${report.configExists ? "" : " (missing)"}`,
528
+ `api: ${report.apiUrl} (${report.health.ok ? "ok" : "failed"})`,
529
+ `workspace: ${report.workspace}`,
530
+ `auth: ${report.auth.ok ? "ok" : `failed — ${report.auth.error ?? "no token"}`}`,
443
531
  ];
444
- if (mismatch)
445
- lines.push(`warning: ${mismatch}`);
446
- for (const h of hints)
447
- if (h !== mismatch)
532
+ if (report.usage) {
533
+ lines.push(report.usage.ok
534
+ ? `usage: ${report.usage.bytes} bytes, ${report.usage.objects} objects, ${report.usage.uploadsInPeriod} uploads this period`
535
+ : `usage: failed ${report.usage.error ?? "unknown"}`);
536
+ }
537
+ if (report.warning)
538
+ lines.push(`warning: ${report.warning}`);
539
+ for (const h of report.hints)
540
+ if (h !== report.warning)
448
541
  lines.push(`hint: ${h}`);
449
542
  await writeStdout(lines.join("\n") + "\n");
450
543
  return report.ok ? 0 : 1;
package/dist/errors.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type UploadsErrorCode = "MISSING_TOKEN" | "NO_PUBLIC_URL" | "NOT_FOUND" | "UNAUTHORIZED" | "INVALID_KEY" | "API_ERROR" | "NETWORK" | "USAGE";
1
+ export type UploadsErrorCode = "MISSING_TOKEN" | "NO_PUBLIC_URL" | "NOT_FOUND" | "UNAUTHORIZED" | "INVALID_KEY" | "STORAGE_QUOTA" | "UPLOAD_BUDGET" | "API_ERROR" | "NETWORK" | "USAGE";
2
2
  export declare class UploadsError extends Error {
3
3
  readonly code: UploadsErrorCode;
4
4
  readonly status?: number;
package/dist/index.d.ts CHANGED
@@ -2,6 +2,6 @@ export { inferContentType, buildMarkdown } from "./embed.js";
2
2
  export { sanitizeKeySegment, sha256Short, deriveRepoFromGit, buildScreenshotKey } from "./keys.js";
3
3
  export { DEFAULT_API_URL, DEFAULT_WORKSPACE, UPLOADS_CONFIG_KEYS, defaultConfigPath, resolveConfigPath, loadConfigFile, loadEnvFile, resolveApiUrl, resolveConfig, describeConfigSources, redactToken, writeConfigKeys, configValuesFromClient, putDefaultsToConfigValues, resolvePutDefaults, mergePutDefaults, workspaceFromToken, workspaceMismatch, type UploadsClientConfig, type ResolvedConfig, type WorkspaceSource, type ConfigValueSource, type ConfigSources, type UploadsConfigKey, type UploadsConfigValues, type PutDefaults, } from "./config.js";
4
4
  export { UploadsError, type UploadsErrorCode } from "./errors.js";
5
- export { createUploadsClient, type UploadsClient, type PutOptions, type ListOptions, type PutResult, type ListItem, type ListResult, type HeadResult, type DeleteResult, type HealthResult, } from "./client.js";
5
+ export { createUploadsClient, type UploadsClient, type PutOptions, type ListOptions, type PutResult, type ListItem, type ListResult, type HeadResult, type DeleteResult, type HealthResult, type UsageResult, type ReconcileResult, type PurgeExpiredResult, type PurgeExpiredResponse, } from "./client.js";
6
6
  export { ATTACHMENTS_MARKER, attachmentsCommentBody, ghAttachmentKey, ghKeyPrefix, isValidRepo, parseRepoFromRemoteUrl, type AttachmentItem, type GhTarget, type GhTargetKind, } from "./github.js";
7
7
  export { execRunner, resolveRepo, upsertAttachmentsComment, type CommandRunner, } from "./github-gh.js";
package/dist/io.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ /** Backpressure-aware stdout helpers shared by the CLI commands and the stdio MCP transport. */
2
+ export declare function writeStdout(text: string): Promise<void>;
3
+ export declare function writeJson(value: unknown): Promise<void>;
package/dist/io.js ADDED
@@ -0,0 +1,9 @@
1
+ /** Backpressure-aware stdout helpers shared by the CLI commands and the stdio MCP transport. */
2
+ export async function writeStdout(text) {
3
+ if (!process.stdout.write(text)) {
4
+ await new Promise((resolve) => process.stdout.once("drain", resolve));
5
+ }
6
+ }
7
+ export async function writeJson(value) {
8
+ await writeStdout(JSON.stringify(value, null, 2) + "\n");
9
+ }
@@ -0,0 +1,4 @@
1
+ export type ToolArgs = Record<string, unknown>;
2
+ export declare function usage(msg: string): never;
3
+ export declare function optString(args: ToolArgs, name: string): string | undefined;
4
+ export declare function optPosInt(args: ToolArgs, name: string): number | undefined;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Argument helpers shared by the stdio MCP tool set (./tools.ts) and the
3
+ * remote worker's tool set (apps/mcp). Runtime-agnostic — usable from
4
+ * Workers as well as Node.
5
+ */
6
+ import { UploadsError } from "../errors.js";
7
+ export function usage(msg) {
8
+ throw new UploadsError(msg, "USAGE");
9
+ }
10
+ export function optString(args, name) {
11
+ const v = args[name];
12
+ if (v === undefined || v === null)
13
+ return undefined;
14
+ if (typeof v !== "string")
15
+ usage(`${name} must be a string`);
16
+ return v;
17
+ }
18
+ export function optPosInt(args, name) {
19
+ const v = args[name];
20
+ if (v === undefined || v === null)
21
+ return undefined;
22
+ if (typeof v !== "number" || !Number.isInteger(v) || v <= 0) {
23
+ usage(`${name} must be a positive integer`);
24
+ }
25
+ return v;
26
+ }
@@ -0,0 +1,19 @@
1
+ export { optPosInt, optString, usage, type ToolArgs } from "./args.js";
2
+ export interface McpTool {
3
+ name: string;
4
+ description: string;
5
+ /** Hand-written JSON Schema for the tool's arguments. */
6
+ inputSchema: Record<string, unknown>;
7
+ handler: (args: Record<string, unknown>) => Promise<unknown>;
8
+ }
9
+ export interface McpServer {
10
+ /** Handle one JSON-RPC line. Undefined for notifications / client responses. */
11
+ handleLine(line: string): Promise<string | undefined>;
12
+ }
13
+ export declare function createMcpServer(opts: {
14
+ serverInfo: {
15
+ name: string;
16
+ version: string;
17
+ };
18
+ tools: McpTool[];
19
+ }): McpServer;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Minimal, dependency-free MCP (Model Context Protocol) server core.
3
+ *
4
+ * Transport is one JSON-RPC 2.0 message per line/request. This module is
5
+ * transport- and runtime-agnostic (usable from Workers as well as Node) —
6
+ * `handleLine` takes a raw message string and returns the serialized response
7
+ * (or undefined when no response is due), so it is directly testable. The
8
+ * stdio transport lives in ./stdio.ts; logs must never go to stdout.
9
+ */
10
+ import { UploadsError } from "../errors.js";
11
+ export { optPosInt, optString, usage } from "./args.js";
12
+ const SUPPORTED_PROTOCOL_VERSIONS = new Set(["2025-06-18", "2025-03-26", "2024-11-05"]);
13
+ const LATEST_PROTOCOL_VERSION = "2025-06-18";
14
+ function response(id, result) {
15
+ return JSON.stringify({ jsonrpc: "2.0", id, result });
16
+ }
17
+ function errorResponse(id, code, message) {
18
+ return JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } });
19
+ }
20
+ /** Tool failures become tool results (isError), never JSON-RPC errors. */
21
+ function toolErrorText(err) {
22
+ if (err instanceof UploadsError)
23
+ return `${err.message} (${err.code})`;
24
+ return err instanceof Error ? err.message : String(err);
25
+ }
26
+ export function createMcpServer(opts) {
27
+ const { serverInfo, tools } = opts;
28
+ async function callTool(id, params) {
29
+ const name = params.name;
30
+ const tool = typeof name === "string" ? tools.find((t) => t.name === name) : undefined;
31
+ if (!tool)
32
+ return errorResponse(id, -32602, `unknown tool: ${String(name ?? "(missing)")}`);
33
+ const args = params.arguments ?? {};
34
+ if (typeof args !== "object" || args === null || Array.isArray(args)) {
35
+ return errorResponse(id, -32602, "tool arguments must be an object");
36
+ }
37
+ try {
38
+ const result = await tool.handler(args);
39
+ return response(id, {
40
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
41
+ structuredContent: result,
42
+ isError: false,
43
+ });
44
+ }
45
+ catch (err) {
46
+ return response(id, {
47
+ content: [{ type: "text", text: toolErrorText(err) }],
48
+ isError: true,
49
+ });
50
+ }
51
+ }
52
+ return {
53
+ async handleLine(line) {
54
+ let msg;
55
+ try {
56
+ msg = JSON.parse(line);
57
+ }
58
+ catch {
59
+ return errorResponse(null, -32700, "Parse error");
60
+ }
61
+ // JSON-RPC batching was removed from MCP: arrays are invalid requests.
62
+ if (typeof msg !== "object" || msg === null || Array.isArray(msg)) {
63
+ return errorResponse(null, -32600, "Invalid Request");
64
+ }
65
+ const record = msg;
66
+ const { method, params } = record;
67
+ // A response from the client (has result/error, no method): ignore.
68
+ if (method === undefined && ("result" in record || "error" in record))
69
+ return undefined;
70
+ const id = typeof record.id === "string" || typeof record.id === "number" ? record.id : null;
71
+ if (typeof method !== "string")
72
+ return errorResponse(id, -32600, "Invalid Request");
73
+ if (method.startsWith("notifications/"))
74
+ return undefined;
75
+ // A request without an id is a notification — never respond.
76
+ if (!("id" in record))
77
+ return undefined;
78
+ const p = (typeof params === "object" && params !== null && !Array.isArray(params) ? params : {});
79
+ try {
80
+ switch (method) {
81
+ case "initialize": {
82
+ const requested = typeof p.protocolVersion === "string" ? p.protocolVersion : "";
83
+ const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.has(requested)
84
+ ? requested
85
+ : LATEST_PROTOCOL_VERSION;
86
+ return response(id, { protocolVersion, capabilities: { tools: {} }, serverInfo });
87
+ }
88
+ case "ping":
89
+ return response(id, {});
90
+ case "tools/list":
91
+ return response(id, {
92
+ tools: tools.map(({ name, description, inputSchema }) => ({
93
+ name,
94
+ description,
95
+ inputSchema,
96
+ })),
97
+ });
98
+ case "tools/call":
99
+ return await callTool(id, p);
100
+ default:
101
+ return errorResponse(id, -32601, `method not found: ${method}`);
102
+ }
103
+ }
104
+ catch (err) {
105
+ return errorResponse(id, -32603, err instanceof Error ? err.message : String(err));
106
+ }
107
+ },
108
+ };
109
+ }
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "./server.js";
2
+ /** Serve the MCP protocol on stdin/stdout; resolves when stdin ends. */
3
+ export declare function serveStdio(server: McpServer): Promise<void>;
@@ -0,0 +1,14 @@
1
+ /** Stdio transport for the MCP server core (Node-only; the core itself is runtime-agnostic). */
2
+ import { createInterface } from "node:readline";
3
+ import { writeStdout } from "../io.js";
4
+ /** Serve the MCP protocol on stdin/stdout; resolves when stdin ends. */
5
+ export async function serveStdio(server) {
6
+ const rl = createInterface({ input: process.stdin, crlfDelay: Infinity, terminal: false });
7
+ for await (const line of rl) {
8
+ if (!line.trim())
9
+ continue;
10
+ const out = await server.handleLine(line);
11
+ if (out !== undefined)
12
+ await writeStdout(out + "\n");
13
+ }
14
+ }
@@ -0,0 +1,10 @@
1
+ import type { GlobalFlags } from "../cli-args.js";
2
+ import { type UploadsClient } from "../client.js";
3
+ import { type UploadsClientConfig } from "../config.js";
4
+ import { type CommandRunner } from "../github-gh.js";
5
+ import type { McpTool } from "./server.js";
6
+ export declare function createUploadsMcpTools(opts: {
7
+ globals: GlobalFlags;
8
+ runner?: CommandRunner;
9
+ clientFactory?: (config: UploadsClientConfig) => UploadsClient;
10
+ }): McpTool[];