@usegraft/mcp 0.1.1 → 1.0.0-beta.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/index.js CHANGED
@@ -2,98 +2,34 @@
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
3
 
4
4
  // src/server.ts
5
- import { readdirSync as readdirSync2, readFileSync as readFileSync2, statSync as statSync2, unlinkSync } from "fs";
6
- import { existsSync as existsSync2 } from "fs";
7
- import { join as join2 } from "path";
5
+ import { unlinkSync } from "fs";
6
+ import { createStorage, storageConfigFromEnv } from "@usegraft/assets";
7
+ import { compile, compileStatic, resolveContained as resolveContained4 } from "@usegraft/compiler";
8
+ import { GraftError as GraftError10 } from "@usegraft/contracts";
8
9
  import {
9
- contentTypeFor,
10
- createStorage,
11
- defaultKeyFor,
12
- storageConfigFromEnv
13
- } from "@usegraft/assets";
14
- import {
15
- compile,
16
- compileStatic,
17
- composeDocument,
18
- parseDocument as parseDocument2,
19
- writeDocumentFile
20
- } from "@usegraft/compiler";
21
- import {
22
- GraftError as GraftError2
23
- } from "@usegraft/contracts";
24
- import {
25
- APPROVAL_HEADER,
26
- AssetRef,
27
10
  createFunctionsHandler,
28
11
  defineFunction,
29
12
  field
30
13
  } from "@usegraft/core";
31
- import {
32
- assertSearchQuery,
33
- decideApproval,
34
- listBranches,
35
- listCompilations,
36
- listPendingApprovals,
37
- openStaticIndex,
38
- resolveBranchScope,
39
- scopeChain,
40
- searchContent
41
- } from "@usegraft/db";
42
- import { describeItem, listItems, loadItem } from "@usegraft/registry";
14
+ import { openStaticIndex, resolveBranchScope, searchContent } from "@usegraft/db";
43
15
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
44
- import { z } from "zod";
45
16
 
46
- // src/content-files.ts
47
- import { existsSync, readdirSync, readFileSync, statSync } from "fs";
48
- import { join, relative, sep } from "path";
49
- import { parseDocument } from "@usegraft/compiler";
17
+ // src/content-hints.ts
18
+ import { findDoc as findDocIn, requireCollection as requireCollectionIn } from "@usegraft/compiler";
19
+ var MCP_HINTS = {
20
+ listCollections: "see list_collections",
21
+ authorDocument: "or author it with write_content."
22
+ };
23
+ var requireCollection = (collections, name) => requireCollectionIn(collections, name, MCP_HINTS);
24
+ var findDoc = (contentDir, collectionName, collection, slug) => findDocIn(contentDir, collectionName, collection, slug, MCP_HINTS);
25
+
26
+ // src/tools/approvals.ts
27
+ import { GraftError as GraftError2 } from "@usegraft/contracts";
28
+ import { decideApproval, listPendingApprovals } from "@usegraft/db";
29
+ import { z as z2 } from "zod";
30
+
31
+ // src/tool-result.ts
50
32
  import { GraftError } from "@usegraft/contracts";
51
- function requireCollection(collections, name) {
52
- const collection = collections[name];
53
- if (!collection) {
54
- const known = Object.keys(collections).join(", ") || "(none registered)";
55
- throw new GraftError({
56
- code: "COLLECTION_NOT_FOUND",
57
- message: `No collection named "${name}" is registered`,
58
- fix: `Use one of the registered collections: ${known} (see list_collections), or add defineCollection({ name: "${name}", \u2026 }) to the schema.`,
59
- details: { collection: name, registered: Object.keys(collections) }
60
- });
61
- }
62
- return collection;
63
- }
64
- function readCollectionDocs(contentDir, collectionName, collection) {
65
- const dir = join(contentDir, collectionName);
66
- if (!existsSync(dir) || !statSync(dir).isDirectory()) return [];
67
- const docs = [];
68
- for (const file of walk(dir)) {
69
- const sourcePath = relative(contentDir, file).split(sep).join("/");
70
- docs.push(parseDocument(readFileSync(file, "utf8"), collection, sourcePath));
71
- }
72
- return docs.sort((a, b) => a.slug.localeCompare(b.slug));
73
- }
74
- function findDoc(contentDir, collectionName, collection, slug) {
75
- const docs = readCollectionDocs(contentDir, collectionName, collection);
76
- const doc = docs.find((candidate) => candidate.slug === slug);
77
- if (!doc) {
78
- const slugs = docs.map((candidate) => candidate.slug);
79
- throw new GraftError({
80
- code: "DOCUMENT_NOT_FOUND",
81
- message: `No document with slug "${slug}" in collection "${collectionName}"`,
82
- fix: slugs.length > 0 ? `Existing slugs: ${slugs.join(", ")}. Use one of those, or author the document with write_content.` : `Collection "${collectionName}" has no documents yet \u2014 author the first one with write_content.`,
83
- details: { collection: collectionName, slug, existing: slugs }
84
- });
85
- }
86
- return doc;
87
- }
88
- function walk(dir) {
89
- const out = [];
90
- for (const name of readdirSync(dir)) {
91
- const full = join(dir, name);
92
- if (statSync(full).isDirectory()) out.push(...walk(full));
93
- else if (/\.mdx?$/.test(name)) out.push(full);
94
- }
95
- return out.sort();
96
- }
97
33
 
98
34
  // src/explain.ts
99
35
  import { ErrorCodes } from "@usegraft/contracts";
@@ -224,9 +160,9 @@ var ERROR_KNOWLEDGE = {
224
160
  meaning: "The Graft server (`graft serve`) has nothing mounted at the requested path \u2014 the request reached the right process but the wrong URL.",
225
161
  typicalCauses: [
226
162
  "A typo in the endpoint path (e.g. /api/fns instead of /api/fn/<name>)",
227
- "Expecting the frontend app's routes on the headless runtime \u2014 graft serve hosts only the function, MCP, and health endpoints"
163
+ "Expecting the frontend app's routes on the headless runtime \u2014 graft serve hosts functions, MCP, authored-content reads, and health"
228
164
  ],
229
- howToRecover: "Use POST /api/fn/<name> for typed functions, POST /api/mcp for the MCP Streamable HTTP surface, or GET /healthz for liveness. The error's details carry the path that missed."
165
+ howToRecover: "Use POST /api/fn/<name> for typed functions, POST /api/mcp for the MCP Streamable HTTP surface, GET /api/content/v1/documents or GET /api/content/v1/search for authored content, or GET /healthz for liveness. The error's details carry the path that missed."
230
166
  },
231
167
  AUTHORITY_MISMATCH: {
232
168
  code: "AUTHORITY_MISMATCH",
@@ -373,9 +309,10 @@ var ERROR_KNOWLEDGE = {
373
309
  meaning: "The operation is human-gated: a pending approval request was filed (details.approvalId) and the call will not run until a human approves it. Destructive functions are always gated; under the 'human' approval policy every mutation is.",
374
310
  typicalCauses: [
375
311
  "Calling a function marked `destructive: true` (deletes or irreversibly overwrites data)",
376
- "Calling any mutation on a deployment whose approvalPolicy is 'human'"
312
+ "Calling any mutation on a deployment whose approvalPolicy is 'human'",
313
+ "Calling a destructive function over MCP at all \u2014 the 'unattended' policy that lifts this gate for a headless deployment is not part of the MCP surface, so there is no server setting that makes this tool call stop asking. That is deliberate, and not something to route around"
377
314
  ],
378
- howToRecover: "Ask a human operator to run `graft approve <approvalId>` (they can also `graft deny` it). Once approved, retry the EXACT same call carrying the approval id \u2014 over MCP pass it as the `approval` tool argument; over raw HTTP send the `x-graft-approval: <approvalId>` header. Approvals are one-shot and bound to the exact input \u2014 never work around the gate."
315
+ howToRecover: "Ask a human operator to run `graft approve <approvalId>` (they can also `graft deny` it). Once approved, retry the EXACT same call carrying the approval id \u2014 over MCP pass it as the `approval` tool argument; over raw HTTP send the `x-graft-approval: <approvalId>` header. Approvals are one-shot and bound to the exact input \u2014 never work around the gate. If you are seeing this over MCP, the server did not ask your client directly: either the mount has not opted into elicited approvals (`approvalElicitation`, off by default and intended for a local server whose operator is present) or your client did not declare the elicitation capability. Both are ordinary \u2014 the gate is the same either way, only the way the human is reached differs."
379
316
  },
380
317
  APPROVAL_INVALID: {
381
318
  code: "APPROVAL_INVALID",
@@ -388,6 +325,16 @@ var ERROR_KNOWLEDGE = {
388
325
  ],
389
326
  howToRecover: "pending \u2192 wait for the human decision; denied \u2192 do not retry, ask the operator; already_consumed or not_found \u2192 call again without the approval id to file a fresh request; mismatch \u2192 retry with exactly the approved input."
390
327
  },
328
+ APPROVAL_UNATTRIBUTED: {
329
+ code: "APPROVAL_UNATTRIBUTED",
330
+ meaning: "The approval was filed by a caller with no stable identity, so there is nobody the request can be held to and no meaningful separation of duties to enforce. It cannot be decided by anyone.",
331
+ typicalCauses: [
332
+ "An unauthenticated caller reached a destructive function (an MCP mount or functions route serving anonymous actors)",
333
+ "A trusted issuer minted a token with no `sub` claim, so the actor authenticated but carried no id",
334
+ "A row left over from before approvals required an identified requester"
335
+ ],
336
+ howToRecover: "Fix the filing surface, not the row: require authentication where the approval was requested (an actor resolver, or a token that carries `sub`), then have the caller request the operation again so the approval records who wants it. A stale row can be resolved directly in the database."
337
+ },
391
338
  APPROVAL_SELF_DECISION: {
392
339
  code: "APPROVAL_SELF_DECISION",
393
340
  meaning: "The identity deciding an approval is the same identity that requested it. Separation of duties: a requester can never approve (or deny) their own destructive operation.",
@@ -486,9 +433,15 @@ function explainCode(code) {
486
433
  return code in ErrorCodes ? ERROR_KNOWLEDGE[code] : void 0;
487
434
  }
488
435
 
489
- // src/server.ts
490
- function ok(payload) {
491
- return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
436
+ // src/tool-result.ts
437
+ var isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
438
+ function ok(payload, links = []) {
439
+ const result = {
440
+ // Text first: a client that ignores the rest still gets the whole answer.
441
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }, ...links]
442
+ };
443
+ if (isPlainObject(payload)) result.structuredContent = payload;
444
+ return result;
492
445
  }
493
446
  function fail(error) {
494
447
  const explanation = ERROR_KNOWLEDGE[error.code];
@@ -506,465 +459,317 @@ function fail(error) {
506
459
  ]
507
460
  };
508
461
  }
509
- async function guarded(body) {
462
+ async function guarded(body, linksFor) {
510
463
  try {
511
- return ok(await body());
464
+ const payload = await body();
465
+ return ok(payload, linksFor?.(payload));
512
466
  } catch (error) {
513
- if (error instanceof GraftError2) return fail(error);
467
+ if (error instanceof GraftError) return fail(error);
514
468
  throw error;
515
469
  }
516
470
  }
517
- function createGraftMcp(options) {
518
- const { contentDir, collections } = options;
519
- const branchId = options.branchId ?? "main";
520
- const staticIndexPath = options.db === void 0 ? options.staticIndexPath : void 0;
521
- const maybeDb = options.db;
522
- if (maybeDb === void 0 && staticIndexPath === void 0) {
523
- throw new GraftError2({
524
- code: "CONFIG_INVALID",
525
- message: "createGraftMcp needs an index: pass `db` (Postgres) or `staticIndexPath`.",
526
- fix: "Pass `db` from createDb(DATABASE_URL), or `staticIndexPath` pointing at the compiled artifact (.graft/index.db) for a static project."
527
- });
471
+ async function guardedResource(body) {
472
+ try {
473
+ return await body();
474
+ } catch (error) {
475
+ if (!(error instanceof GraftError)) throw error;
476
+ const explanation = ERROR_KNOWLEDGE[error.code];
477
+ throw new Error(
478
+ [error.message, error.fix && `Fix: ${error.fix}`, explanation?.howToRecover].filter(Boolean).join(" "),
479
+ { cause: error }
480
+ );
528
481
  }
529
- const requireDb = (feature, insteadDo) => {
530
- if (maybeDb !== void 0) return maybeDb;
531
- throw new GraftError2({
532
- code: "NEEDS_DATABASE",
533
- message: `${feature} needs the Postgres index; this project serves a static index (${staticIndexPath}).`,
534
- fix: `${insteadDo} To move this project to the Postgres tier: set DATABASE_URL, change graft.config to \`export const index = "postgres"\`, run \`graft db migrate\`, then \`graft compile\`.`,
535
- details: { feature, index: "static" }
536
- });
537
- };
538
- const projectContent = async () => staticIndexPath === void 0 ? compile({ contentDir, collections, db: requireDb("compile", ""), branchId }) : compileStatic({ contentDir, collections, indexPath: staticIndexPath });
539
- const searchIndex = async (query) => {
540
- if (staticIndexPath === void 0) {
541
- return searchContent(requireDb("search_content", ""), query);
542
- }
543
- const index = await openStaticIndex(staticIndexPath);
544
- try {
545
- return await index.searchContent({
546
- query: query.query,
547
- collections: query.collections,
548
- limit: query.limit
549
- });
550
- } finally {
551
- await index.close();
552
- }
553
- };
554
- const functions = options.functions ?? {};
555
- const functionsByName = /* @__PURE__ */ new Map();
556
- for (const fn of Object.values(functions)) functionsByName.set(fn.name, fn);
557
- let scopePromise;
558
- const getScope = () => {
559
- scopePromise ??= options.scope ? Promise.resolve(options.scope) : resolveBranchScope(requireDb("Branch scope resolution", ""), branchId);
560
- return scopePromise;
561
- };
562
- let functionsHandler;
563
- const getFunctionsHandler = () => {
564
- functionsHandler ??= createFunctionsHandler({
565
- functions,
566
- db: requireDb(
567
- "run_function",
568
- "Typed functions read and write operational data in Postgres, so a static project has none."
569
- ),
570
- branch: branchId,
571
- actor: options.actor,
572
- approvalPolicy: options.approvalPolicy,
573
- rateLimit: options.rateLimit,
574
- gitSha: options.gitSha,
575
- audit: options.audit,
576
- approvals: options.approvals
577
- });
578
- return functionsHandler;
579
- };
580
- const deleteContentFn = defineFunction({
581
- name: "delete_content",
582
- kind: "mutation",
583
- destructive: true,
584
- public: true,
585
- description: "Delete an authored MDX document and recompile (MCP delete_content tool).",
586
- returns: "{ deleted, branch, gitSha, changes }",
587
- input: {
588
- collection: field.string({ description: "Collection name" }),
589
- slug: field.string({ description: "Document slug to delete" })
590
- },
591
- handler: async ({ input }) => {
592
- const collection = requireCollection(collections, input.collection);
593
- const doc = findDoc(contentDir, input.collection, collection, input.slug);
594
- unlinkSync(join2(contentDir, ...doc.sourcePath.split("/")));
595
- const result = await projectContent();
596
- return {
597
- deleted: doc.sourcePath,
598
- branch: branchId,
599
- gitSha: result.gitSha,
600
- changes: result.changes
601
- };
602
- }
603
- });
604
- let deleteHandler;
605
- const getDeleteHandler = () => {
606
- deleteHandler ??= createFunctionsHandler({
607
- // The one-shot, input-bound human approval lives in Postgres. Rather than
608
- // silently downgrading to an ungated delete, a static project is told to
609
- // do it the way git already makes safe: delete the file and recompile.
610
- db: requireDb(
611
- "delete_content",
612
- "Its human approval gate is a Postgres table, and dropping the gate would make the delete ungated. In a static project, delete the file and recompile \u2014 git is authoritative, so the file IS the document and git history is the undo."
613
- ),
614
- functions: { delete_content: deleteContentFn },
615
- branch: branchId,
616
- actor: options.actor,
617
- rateLimit: options.rateLimit,
618
- gitSha: options.gitSha,
619
- audit: options.audit,
620
- approvals: options.approvals
621
- });
622
- return deleteHandler;
623
- };
624
- let storagePromise;
625
- const getStorage = () => {
626
- storagePromise ??= (async () => {
627
- if (options.storage) {
628
- return typeof options.storage === "function" ? options.storage() : options.storage;
629
- }
630
- try {
631
- return createStorage(storageConfigFromEnv());
632
- } catch (error) {
633
- throw new GraftError2({
634
- code: "ENV_VAR_MISSING",
635
- message: error instanceof Error ? error.message : String(error),
636
- fix: "Set S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY, and S3_BUCKET in the MCP server's environment (.env), then retry.",
637
- details: { variables: ["S3_ENDPOINT", "S3_ACCESS_KEY", "S3_SECRET_KEY", "S3_BUCKET"] }
638
- });
639
- }
640
- })();
641
- return storagePromise;
642
- };
643
- const server = new McpServer({
644
- name: options.name ?? "graft",
645
- version: options.version ?? "0.0.0"
646
- });
482
+ }
483
+
484
+ // src/tools/annotations.ts
485
+ var READS = {
486
+ readOnlyHint: true,
487
+ openWorldHint: false
488
+ };
489
+ var WRITES = {
490
+ readOnlyHint: false,
491
+ destructiveHint: false,
492
+ idempotentHint: false,
493
+ openWorldHint: false
494
+ };
495
+ var DESTROYS = {
496
+ readOnlyHint: false,
497
+ destructiveHint: true,
498
+ idempotentHint: false,
499
+ openWorldHint: false
500
+ };
501
+
502
+ // src/tools/outputs.ts
503
+ import { FunctionDescriptor, RegistryItemDescriptor, SchemaDescription } from "@usegraft/contracts";
504
+ import { z } from "zod";
505
+ var DocumentData = z.record(z.string(), z.unknown());
506
+ var ChangeSet = z.object({
507
+ added: z.array(z.string()),
508
+ changed: z.array(z.string()),
509
+ removed: z.array(z.string()),
510
+ unchanged: z.number()
511
+ });
512
+ var listCollectionsOutput = {
513
+ branch: z.string(),
514
+ collections: z.array(
515
+ z.object({
516
+ name: z.string(),
517
+ description: z.string().optional(),
518
+ authority: z.string(),
519
+ fields: z.number()
520
+ })
521
+ )
522
+ };
523
+ var describeSchemaOutput = SchemaDescription.shape;
524
+ var listFunctionsOutput = {
525
+ branch: z.string(),
526
+ functions: z.array(
527
+ z.object({
528
+ name: z.string(),
529
+ kind: z.string(),
530
+ description: z.string().optional(),
531
+ public: z.boolean().optional(),
532
+ destructive: z.boolean().optional(),
533
+ args: z.number()
534
+ })
535
+ )
536
+ };
537
+ var describeFunctionOutput = FunctionDescriptor.shape;
538
+ var listRegistryOutput = {
539
+ items: z.array(
540
+ z.object({
541
+ name: z.string(),
542
+ type: z.string(),
543
+ description: z.string(),
544
+ registryDependencies: z.array(z.string()).optional()
545
+ })
546
+ )
547
+ };
548
+ var describeItemOutput = RegistryItemDescriptor.shape;
549
+ var listPackagesOutput = {
550
+ packages: z.array(
551
+ z.object({
552
+ name: z.string(),
553
+ role: z.string(),
554
+ when: z.string(),
555
+ tier: z.enum(["static", "postgres", "either"]),
556
+ framework: z.string().optional(),
557
+ direct: z.boolean()
558
+ })
559
+ )
560
+ };
561
+ var listContentOutput = {
562
+ collection: z.string(),
563
+ documents: z.array(
564
+ z.object({
565
+ slug: z.string(),
566
+ sourcePath: z.string(),
567
+ data: DocumentData
568
+ })
569
+ )
570
+ };
571
+ var getContentOutput = {
572
+ collection: z.string(),
573
+ slug: z.string(),
574
+ sourcePath: z.string(),
575
+ data: DocumentData,
576
+ body: z.string()
577
+ };
578
+ var searchContentOutput = {
579
+ branch: z.string(),
580
+ /** The resolved overlay chain, leaf-first — which branches were searched. */
581
+ chain: z.array(z.string()),
582
+ query: z.string(),
583
+ hits: z.array(
584
+ z.object({
585
+ collection: z.string(),
586
+ slug: z.string(),
587
+ sourcePath: z.string(),
588
+ rank: z.number(),
589
+ snippet: z.string(),
590
+ data: DocumentData
591
+ })
592
+ )
593
+ };
594
+ var writeContentOutput = {
595
+ written: z.string(),
596
+ branch: z.string(),
597
+ /** Null when the content tree is not in a git checkout. */
598
+ gitSha: z.string().nullable(),
599
+ changes: ChangeSet
600
+ };
601
+ var listBranchesOutput = {
602
+ branches: z.array(
603
+ z.object({
604
+ name: z.string(),
605
+ parent: z.string().nullable(),
606
+ backend: z.string(),
607
+ status: z.string(),
608
+ createdAt: z.string(),
609
+ endpointHost: z.string().nullable()
610
+ })
611
+ )
612
+ };
613
+ var listCompilationsOutput = {
614
+ compilations: z.array(
615
+ z.object({
616
+ id: z.string(),
617
+ branchId: z.string(),
618
+ gitSha: z.string().nullable(),
619
+ docCount: z.number(),
620
+ added: z.number(),
621
+ changed: z.number(),
622
+ removed: z.number(),
623
+ createdAt: z.string()
624
+ })
625
+ )
626
+ };
627
+ var listApprovalsOutput = {
628
+ approvals: z.array(
629
+ z.object({
630
+ id: z.string(),
631
+ branchId: z.string(),
632
+ functionName: z.string(),
633
+ /** The canonical input the approval is bound to; jsonb, so open. */
634
+ input: z.unknown(),
635
+ requestedByKind: z.string(),
636
+ requestedById: z.string().nullable(),
637
+ correlationId: z.string().nullable(),
638
+ createdAt: z.string()
639
+ })
640
+ )
641
+ };
642
+ var decideApprovalOutput = {
643
+ id: z.string(),
644
+ status: z.string(),
645
+ decidedBy: z.string().nullable(),
646
+ functionName: z.string()
647
+ };
648
+ var putAssetOutput = {
649
+ key: z.string(),
650
+ contentType: z.string(),
651
+ bytes: z.number(),
652
+ url: z.string(),
653
+ /** A ready-to-paste `field.asset` frontmatter snippet. */
654
+ frontmatter: z.string()
655
+ };
656
+ var explainErrorOutput = {
657
+ code: z.string().optional(),
658
+ known: z.boolean().optional(),
659
+ meaning: z.string().optional(),
660
+ typicalCauses: z.array(z.string()).optional(),
661
+ howToRecover: z.string().optional(),
662
+ /** The `fix` off the actual error, which beats the general advice. */
663
+ specificFix: z.string().optional(),
664
+ message: z.string().optional(),
665
+ knownCodes: z.array(z.string()).optional(),
666
+ hint: z.string().optional()
667
+ };
668
+
669
+ // src/tools/approvals.ts
670
+ var registerApprovalTools = (server, deps) => {
671
+ const { branchId, requireDb, requireDecider, requireScope } = deps;
647
672
  server.registerTool(
648
- "list_collections",
673
+ "list_approvals",
649
674
  {
650
- title: "List collections",
651
- description: "List every registered content collection (name, description, authority, field count). Start here to learn what kinds of content this project has.",
675
+ title: "List pending approvals",
676
+ outputSchema: listApprovalsOutput,
677
+ annotations: READS,
678
+ description: "Pending human-gated approvals. Decide with decide_approval, Studio Approve/Deny, or `graft approve` / `graft deny`. Same data as GET /api/studio/v1/approvals.",
652
679
  inputSchema: {}
653
680
  },
654
- () => guarded(() => ({
655
- branch: branchId,
656
- collections: Object.values(collections).map((collection) => {
657
- const descriptor = collection.describe();
658
- return {
659
- name: descriptor.name,
660
- description: descriptor.description,
661
- authority: descriptor.authority,
662
- fields: descriptor.fields.length
663
- };
664
- })
681
+ () => guarded(async () => ({
682
+ approvals: (await listPendingApprovals(
683
+ requireDb(
684
+ "list_approvals",
685
+ "Approvals gate destructive operations on operational data, which a static project does not have."
686
+ )
687
+ )).map((row) => ({
688
+ id: row.id,
689
+ branchId: row.branchId,
690
+ functionName: row.functionName,
691
+ input: row.input,
692
+ requestedByKind: row.requestedByKind,
693
+ requestedById: row.requestedById,
694
+ correlationId: row.correlationId,
695
+ createdAt: row.createdAt.toISOString()
696
+ }))
665
697
  }))
666
698
  );
667
699
  server.registerTool(
668
- "describe_schema",
700
+ "decide_approval",
669
701
  {
670
- title: "Describe the content schema",
671
- description: "Full schema introspection: every collection with its typed fields (name, type, optional, description), plus every registered function (kind, args, public/destructive). Documents also accept an optional kebab-case `slug` (defaults to the filename). Prefer list_functions / describe_function when you only need the function surface.",
672
- inputSchema: {}
702
+ title: "Approve or deny a pending approval",
703
+ outputSchema: decideApprovalOutput,
704
+ annotations: DESTROYS,
705
+ description: "Record a decision on a pending approval (same as Studio Approve/Deny and `graft approve` / `graft deny`). The decision is attributed to the identity THIS connection authenticated as \u2014 there is no way to name a different decider \u2014 and a requester can never decide their own approval. Requires an authenticated caller and an owner DB role that can UPDATE approvals.",
706
+ inputSchema: {
707
+ id: z2.string().describe("Pending approval id from list_approvals"),
708
+ decision: z2.enum(["approved", "denied"]).describe("approved or denied")
709
+ }
673
710
  },
674
- () => guarded(() => {
711
+ ({ id, decision }) => guarded(async () => {
712
+ requireScope("decide_approval", "approvals:decide");
713
+ const row = await decideApproval(
714
+ requireDb(
715
+ "decide_approval",
716
+ "Approvals gate destructive operations on operational data, which a static project does not have."
717
+ ),
718
+ id,
719
+ decision,
720
+ requireDecider()
721
+ );
722
+ if (!row) {
723
+ throw new GraftError2({
724
+ code: "APPROVAL_INVALID",
725
+ message: `No PENDING approval "${id}" exists \u2014 it may already be decided, consumed, or mistyped.`,
726
+ fix: "Call list_approvals and use a pending id.",
727
+ details: { id }
728
+ });
729
+ }
675
730
  return {
676
- collections: Object.values(collections).map((collection) => {
677
- const descriptor = collection.describe();
678
- return { ...descriptor, fields: descriptor.fields.map(teachAssetFields) };
679
- }),
680
- functions: [...functionsByName.values()].map((fn) => fn.describe())
681
- };
682
- })
683
- );
684
- server.registerTool(
685
- "list_functions",
686
- {
687
- title: "List functions",
688
- description: "List every registered typed function (name, kind, public, destructive, short description). Use describe_function for the full input schema, then run_function to invoke. Mutations reject anonymous callers unless public: true; destructive functions always require human approval (graft approve).",
689
- inputSchema: {}
690
- },
691
- () => guarded(() => ({
692
- branch: branchId,
693
- functions: [...functionsByName.values()].map((fn) => {
694
- const d = fn.describe();
695
- return {
696
- name: d.name,
697
- kind: d.kind,
698
- description: d.description,
699
- public: d.public,
700
- destructive: d.destructive,
701
- args: d.args.length
702
- };
703
- })
704
- }))
705
- );
706
- server.registerTool(
707
- "describe_function",
708
- {
709
- title: "Describe one function",
710
- description: "Full introspection for one function: kind, args (name/type/optional/description), returns, public, destructive. Use this before run_function so the input object matches the schema.",
711
- inputSchema: {
712
- name: z.string().describe("Function name as returned by list_functions")
713
- }
714
- },
715
- ({ name }) => guarded(() => {
716
- const fn = functionsByName.get(name);
717
- if (!fn) {
718
- throw new GraftError2({
719
- code: "FUNCTION_NOT_FOUND",
720
- message: `No function named "${name}" is registered.`,
721
- fix: `Call list_functions and use one of: ${[...functionsByName.keys()].join(", ") || "(none registered)"}.`,
722
- details: { requested: name, available: [...functionsByName.keys()] }
723
- });
724
- }
725
- return fn.describe();
726
- })
727
- );
728
- server.registerTool(
729
- "run_function",
730
- {
731
- title: "Run a typed function",
732
- description: "Invoke a defineFunction by name with a JSON input object. Same pipeline as POST /api/fn/<name>: Zod validation, access rules, rate limits, audit log, and the human gate for destructive ops. The server may already act with a configured identity (graft mcp uses GRAFT_DEV_TOKEN; over HTTP your connection's bearer is forwarded) \u2014 only pass authorization to override it. Pass approval after a human runs `graft approve <id>` for gated calls. Success returns { data, correlationId }; failures are GraftError JSON with a fix.",
733
- inputSchema: {
734
- name: z.string().describe("Function name (defineFunction name, not the export key)"),
735
- input: z.record(z.string(), z.unknown()).optional().describe("Input fields object; defaults to {}. See describe_function for the schema."),
736
- authorization: z.string().optional().describe(
737
- "Bearer token override (with or without the 'Bearer ' prefix). Usually unnecessary \u2014 the server's configured identity applies when omitted."
738
- ),
739
- approval: z.string().optional().describe(
740
- "Approval id from a prior DESTRUCTIVE_OP_REQUIRES_APPROVAL response (after `graft approve <id>`)."
741
- )
742
- }
743
- },
744
- ({ name, input, authorization, approval }) => guarded(async () => {
745
- if (functionsByName.size === 0) {
746
- throw new GraftError2({
747
- code: "FUNCTION_NOT_FOUND",
748
- message: "This MCP server has no functions registered.",
749
- fix: "Export `functions` from graft.config.ts (defineFunction results, often via mergePrimitives) and restart the MCP server / pass them to createGraftMcp({ functions }).",
750
- details: { requested: name, available: [] }
751
- });
752
- }
753
- if (!functionsByName.has(name)) {
754
- throw new GraftError2({
755
- code: "FUNCTION_NOT_FOUND",
756
- message: `No function named "${name}" is registered.`,
757
- fix: `Call list_functions and use one of: ${[...functionsByName.keys()].join(", ")}.`,
758
- details: { requested: name, available: [...functionsByName.keys()] }
759
- });
760
- }
761
- return invokeFunction(getFunctionsHandler(), name, input ?? {}, {
762
- credential: authorization ?? options.defaultAuthorization,
763
- approval
764
- });
765
- })
766
- );
767
- server.registerTool(
768
- "list_registry",
769
- {
770
- title: "List registry items",
771
- description: "List every owned primitive available to `graft add` \u2014 shadcn-style copy-in blocks / fields / access rules / bundles (name, type, one-line description, and any registry items it pulls in). Use describe_item for the full details, then install with `graft add <name>` from the CLI. MCP browses what exists; the CLI installs it.",
772
- inputSchema: {}
773
- },
774
- () => guarded(() => ({
775
- items: listItems(options.registryRoot).map((item) => ({
776
- name: item.name,
777
- type: item.type,
778
- description: item.description,
779
- registryDependencies: item.registryDependencies
780
- }))
781
- }))
782
- );
783
- server.registerTool(
784
- "describe_item",
785
- {
786
- title: "Describe a registry item",
787
- description: "Full details for one owned primitive: type, description, the files it writes into the project, npm dependencies to install, the registry items it pulls in first, and whether it ships an llms.txt fragment. Use list_registry for names; install with `graft add <name>` (CLI). MCP does not install.",
788
- inputSchema: {
789
- name: z.string().describe("Item name as returned by list_registry")
790
- }
791
- },
792
- ({ name }) => guarded(() => describeItem(loadItem(name, options.registryRoot)))
793
- );
794
- server.registerTool(
795
- "list_content",
796
- {
797
- title: "List documents in a collection",
798
- description: "List every document in a collection, read from the authored MDX files (git is the source of truth). Returns slug, sourcePath, and frontmatter data.",
799
- inputSchema: {
800
- collection: z.string().describe("Collection name, as returned by list_collections")
801
- }
802
- },
803
- ({ collection: name }) => guarded(() => {
804
- const collection = requireCollection(collections, name);
805
- const docs = readCollectionDocs(contentDir, name, collection);
806
- return {
807
- collection: name,
808
- documents: docs.map((doc) => ({
809
- slug: doc.slug,
810
- sourcePath: doc.sourcePath,
811
- data: doc.data
812
- }))
813
- };
814
- })
815
- );
816
- server.registerTool(
817
- "get_content",
818
- {
819
- title: "Get one document",
820
- description: "Read a single document by collection + slug from the authored MDX files: validated frontmatter data, MDX body, and the file path to edit.",
821
- inputSchema: {
822
- collection: z.string().describe("Collection name"),
823
- slug: z.string().describe("Document slug (kebab-case)")
824
- }
825
- },
826
- ({ collection: name, slug }) => guarded(() => {
827
- const collection = requireCollection(collections, name);
828
- const doc = findDoc(contentDir, name, collection, slug);
829
- return {
830
- collection: name,
831
- slug: doc.slug,
832
- sourcePath: doc.sourcePath,
833
- data: doc.data,
834
- body: doc.body
835
- };
836
- })
837
- );
838
- server.registerTool(
839
- "search_content",
840
- {
841
- title: "Full-text search across content",
842
- description: 'Search authored content by words, "quoted phrases", `or`, and -exclusions (websearch syntax). Searches the branch\'s effective content in the compiled Postgres index \u2014 on a preview branch that includes documents inherited from parent branches, with branch overrides winning \u2014 so results are as fresh as the last compile (write_content compiles automatically); every hit carries the sourcePath of the file to edit. Ranking weights slug matches over frontmatter over body.',
843
- inputSchema: {
844
- query: z.string().describe('What to find, e.g. pricing "free tier" -enterprise'),
845
- collection: z.string().optional().describe("Restrict to one collection (default: all registered collections)"),
846
- limit: z.number().optional().describe("Max hits, best-ranked first (default 20)")
847
- }
848
- },
849
- ({ query, collection: name, limit }) => guarded(async () => {
850
- if (name !== void 0) requireCollection(collections, name);
851
- assertSearchQuery(query);
852
- const collectionNames = name === void 0 ? Object.keys(collections) : [name];
853
- const chain = staticIndexPath === void 0 ? scopeChain(await getScope()) : [branchId];
854
- const hits = await searchIndex({ query, chain, collections: collectionNames, limit });
855
- return {
856
- branch: branchId,
857
- chain,
858
- query,
859
- hits: hits.map(({ row, rank, snippet }) => ({
860
- collection: row.collection,
861
- slug: row.slug,
862
- sourcePath: row.sourcePath,
863
- rank,
864
- snippet,
865
- data: row.data
866
- }))
867
- };
868
- })
869
- );
870
- server.registerTool(
871
- "write_content",
872
- {
873
- title: "Write a document (create or update)",
874
- description: "Author or update a document: validates the data against the collection schema, writes <contentDir>/<collection>/<slug>.mdx, and compiles the content tree into the database. Returns exactly what changed. Git is the version history: commit the file afterwards if you have the server's checkout; remote callers can't and needn't \u2014 the checkout's operator owns the commit.",
875
- inputSchema: {
876
- collection: z.string().describe("Collection name"),
877
- slug: z.string().describe("Document slug \u2014 kebab-case; becomes the filename and the URL segment"),
878
- data: z.record(z.string(), z.unknown()).describe("Frontmatter data; must satisfy the collection schema (see describe_schema)"),
879
- body: z.string().optional().describe("MDX body (markdown). Defaults to empty.")
880
- }
881
- },
882
- ({ collection: name, slug, data, body }) => guarded(async () => {
883
- const collection = requireCollection(collections, name);
884
- if (collection.authority === "db-authoritative") {
885
- throw new GraftError2({
886
- code: "AUTHORITY_MISMATCH",
887
- message: `Collection "${name}" is db-authoritative \u2014 its records live in Postgres, not as MDX files.`,
888
- fix: `Write this data through the collection's function endpoint (POST /api/fn/<name>, see llms.txt) instead of write_content. write_content is only for file-authoritative collections.`,
889
- details: { collection: name, authority: collection.authority }
890
- });
891
- }
892
- const frontmatterSlug = data.slug;
893
- if (frontmatterSlug !== void 0 && frontmatterSlug !== slug) {
894
- throw new GraftError2({
895
- code: "INVALID_SLUG",
896
- message: `data.slug ("${String(frontmatterSlug)}") conflicts with the slug argument ("${slug}")`,
897
- fix: "Omit `slug` from data \u2014 the slug argument names the file and the document.",
898
- details: { slug, frontmatterSlug }
899
- });
900
- }
901
- const sourcePath = `${name}/${slug}.mdx`;
902
- const fullPath = join2(contentDir, ...sourcePath.split("/"));
903
- const existingRaw = existsSync2(fullPath) ? readFileSync2(fullPath, "utf8") : void 0;
904
- const raw = composeDocument(existingRaw, data, body ?? "");
905
- parseDocument2(raw, collection, sourcePath);
906
- assertSlugFree(contentDir, name, collection, slug, sourcePath);
907
- writeDocumentFile(fullPath, raw);
908
- const result = await projectContent();
909
- return {
910
- written: sourcePath,
911
- branch: branchId,
912
- gitSha: result.gitSha,
913
- changes: result.changes
731
+ id: row.id,
732
+ status: row.status,
733
+ decidedBy: row.decidedBy,
734
+ functionName: row.functionName
914
735
  };
915
736
  })
916
737
  );
917
- server.registerTool(
918
- "delete_content",
919
- {
920
- title: "Delete a document (human-gated)",
921
- description: "Delete an authored document: removes <contentDir>/<collection>/<slug>.mdx and compiles, so the index soft-deletes it. DESTRUCTIVE and always human-gated \u2014 the first call files an approval and fails with its id; a human decides with `graft approve <id>` (or deny); then retry the SAME collection+slug with `approval: <id>` (the MCP form of the x-graft-approval header). Approvals are one-shot and bound to that exact input. Git is the version history: commit the deletion afterwards if you have the server's checkout; remote callers can't and needn't \u2014 the checkout's operator owns the commit.",
922
- inputSchema: {
923
- collection: z.string().describe("Collection name"),
924
- slug: z.string().describe("Document slug to delete"),
925
- approval: z.string().optional().describe(
926
- "Approval id from a prior DESTRUCTIVE_OP_REQUIRES_APPROVAL response, after a human ran `graft approve <id>`."
927
- )
928
- }
929
- },
930
- ({ collection: name, slug, approval }) => guarded(async () => {
931
- const collection = requireCollection(collections, name);
932
- if (collection.authority === "db-authoritative") {
933
- throw new GraftError2({
934
- code: "AUTHORITY_MISMATCH",
935
- message: `Collection "${name}" is db-authoritative \u2014 its records live in Postgres, not as MDX files.`,
936
- fix: "Delete records through the collection's typed functions (a destructive defineFunction over deleteRecord \u2014 see list_functions), not delete_content. delete_content is only for file-authoritative collections.",
937
- details: { collection: name, authority: collection.authority }
938
- });
939
- }
940
- findDoc(contentDir, name, collection, slug);
941
- const { data, correlationId } = await invokeFunction(
942
- getDeleteHandler(),
943
- "delete_content",
944
- { collection: name, slug },
945
- { credential: options.defaultAuthorization, approval }
946
- );
947
- return { ...data, correlationId };
948
- })
949
- );
738
+ };
739
+
740
+ // src/tools/assets.ts
741
+ import { readFileSync } from "fs";
742
+ import { contentTypeFor, defaultKeyFor } from "@usegraft/assets";
743
+ import { AssetRef } from "@usegraft/core";
744
+ import { resolveContained } from "@usegraft/compiler";
745
+ import { GraftError as GraftError3 } from "@usegraft/contracts";
746
+ import { z as z3 } from "zod";
747
+ var registerAssetTools = (server, deps) => {
748
+ const { getStorage, options, requireScope } = deps;
749
+ const uploadRoot = options.localUploadRoot;
950
750
  server.registerTool(
951
751
  "put_asset",
952
752
  {
953
753
  title: "Upload an asset (image / binary)",
954
- description: "Upload a binary to the asset store and get the frontmatter reference for an `asset` field. Pass `path` (a file on the machine running this MCP server \u2014 the stdio case) OR `base64` + `key` (remote agents send the bytes). Refuses to overwrite an existing key unless overwrite: true \u2014 the store keeps no version history. Then reference the returned key from an asset field via write_content.",
754
+ outputSchema: putAssetOutput,
755
+ annotations: DESTROYS,
756
+ description: (uploadRoot ? "Upload a binary to the asset store and get the frontmatter reference for an `asset` field. Pass `path` (a file inside this project) OR `base64` + `key`. " : "Upload a binary to the asset store and get the frontmatter reference for an `asset` field. Pass `base64` + `key` \u2014 this server reads no files from its own disk. ") + "Refuses to overwrite an existing key unless overwrite: true \u2014 the store keeps no version history. Then reference the returned key from an asset field via write_content.",
955
757
  inputSchema: {
956
- key: z.string().optional().describe(
758
+ key: z3.string().optional().describe(
957
759
  'Asset key \u2014 a lowercase path like "pages/pricing/hero.png". Required with base64; defaults to assets/<filename> with path.'
958
760
  ),
959
- path: z.string().optional().describe("Path to a file on the MCP server's machine (local/stdio agents)."),
960
- base64: z.string().optional().describe("The file's bytes, base64-encoded (remote/HTTP agents)."),
961
- contentType: z.string().optional().describe("MIME type. Defaults to an inference from the key/path extension."),
962
- overwrite: z.boolean().optional().describe("Replace an existing binary at this key. Off by default.")
761
+ path: z3.string().optional().describe(
762
+ uploadRoot ? `Path to a file inside ${uploadRoot} (local/stdio agents).` : "Not available on this server \u2014 send the bytes as `base64` instead."
763
+ ),
764
+ base64: z3.string().optional().describe("The file's bytes, base64-encoded (remote/HTTP agents)."),
765
+ contentType: z3.string().optional().describe("MIME type. Defaults to an inference from the key/path extension."),
766
+ overwrite: z3.boolean().optional().describe("Replace an existing binary at this key. Off by default.")
963
767
  }
964
768
  },
965
769
  ({ key: keyArg, path, base64, contentType, overwrite }) => guarded(async () => {
770
+ requireScope("put_asset", "content:write");
966
771
  if (path === void 0 === (base64 === void 0)) {
967
- throw new GraftError2({
772
+ throw new GraftError3({
968
773
  code: "INPUT_VALIDATION_FAILED",
969
774
  message: "Pass exactly one of `path` (a file on the MCP server's machine) or `base64` (the file's bytes).",
970
775
  fix: "Local/stdio agents: pass path. Remote/HTTP agents: read the file yourself and pass base64 + key."
@@ -972,10 +777,22 @@ function createGraftMcp(options) {
972
777
  }
973
778
  let bytes;
974
779
  if (path !== void 0) {
780
+ if (uploadRoot === void 0) {
781
+ throw new GraftError3({
782
+ code: "UNAUTHORIZED",
783
+ message: "This server does not read files from its own disk.",
784
+ fix: "Read the file yourself and send its bytes as `base64` with a `key`. Reading server-local paths is only available to a local stdio server, which grants it explicitly.",
785
+ details: { tool: "put_asset" }
786
+ });
787
+ }
788
+ const full = resolveContained(uploadRoot, path, {
789
+ label: "asset source",
790
+ allowAbsolute: true
791
+ });
975
792
  try {
976
- bytes = readFileSync2(path);
793
+ bytes = readFileSync(full);
977
794
  } catch {
978
- throw new GraftError2({
795
+ throw new GraftError3({
979
796
  code: "DOCUMENT_NOT_FOUND",
980
797
  message: `File not found: ${path}`,
981
798
  fix: "Pass a path to a file that exists on the machine running this MCP server, or send the bytes as base64 instead.",
@@ -984,7 +801,7 @@ function createGraftMcp(options) {
984
801
  }
985
802
  } else {
986
803
  if (!/^[A-Za-z0-9+/=\s]+$/.test(base64)) {
987
- throw new GraftError2({
804
+ throw new GraftError3({
988
805
  code: "INPUT_VALIDATION_FAILED",
989
806
  message: "`base64` contains characters outside the base64 alphabet.",
990
807
  fix: "Encode the file's raw bytes as standard base64 (A-Z a-z 0-9 + / =). To upload a file by its location on the server's machine, use `path` instead."
@@ -994,7 +811,7 @@ function createGraftMcp(options) {
994
811
  }
995
812
  const key = keyArg ?? (path !== void 0 ? defaultKeyFor(path) : void 0);
996
813
  if (key === void 0) {
997
- throw new GraftError2({
814
+ throw new GraftError3({
998
815
  code: "INPUT_VALIDATION_FAILED",
999
816
  message: "`key` is required when uploading via base64.",
1000
817
  fix: 'Pass a lowercase path key naming the asset, e.g. "pages/pricing/hero.png".'
@@ -1002,7 +819,7 @@ function createGraftMcp(options) {
1002
819
  }
1003
820
  const keyCheck = AssetRef.shape.key.safeParse(key);
1004
821
  if (!keyCheck.success) {
1005
- throw new GraftError2({
822
+ throw new GraftError3({
1006
823
  code: "INPUT_VALIDATION_FAILED",
1007
824
  message: `"${key}" is not a valid asset key.`,
1008
825
  fix: 'Use a lowercase path of letters, digits, ".", "_", "-" with "/" separators, each segment starting alphanumeric \u2014 e.g. "pages/pricing/hero.png".',
@@ -1011,7 +828,7 @@ function createGraftMcp(options) {
1011
828
  }
1012
829
  const storage = await getStorage();
1013
830
  if (overwrite !== true && await storage.exists(key)) {
1014
- throw new GraftError2({
831
+ throw new GraftError3({
1015
832
  code: "ASSET_EXISTS",
1016
833
  message: `Asset key "${key}" already holds a binary.`,
1017
834
  fix: "Pick a distinct key (the store keeps no version history), or pass overwrite: true if replacing the existing binary is the actual intent.",
@@ -1031,10 +848,19 @@ function createGraftMcp(options) {
1031
848
  };
1032
849
  })
1033
850
  );
851
+ };
852
+
853
+ // src/tools/branches.ts
854
+ import { listBranches, listCompilations } from "@usegraft/db";
855
+ import { z as z4 } from "zod";
856
+ var registerBranchTools = (server, deps) => {
857
+ const { branchId, requireDb } = deps;
1034
858
  server.registerTool(
1035
859
  "list_branches",
1036
860
  {
1037
861
  title: "List branches",
862
+ outputSchema: listBranchesOutput,
863
+ annotations: READS,
1038
864
  description: "List registered content branches (name, parent, backend, status). Same data as GET /api/studio/v1/branches and `graft branch`.",
1039
865
  inputSchema: {}
1040
866
  },
@@ -1058,10 +884,12 @@ function createGraftMcp(options) {
1058
884
  "list_compilations",
1059
885
  {
1060
886
  title: "List compilations",
887
+ outputSchema: listCompilationsOutput,
888
+ annotations: READS,
1061
889
  description: "Recent content projection trail rows (git SHA, added/changed/removed counts), newest first. Same data as GET /api/studio/v1/compilations and `graft compilations`.",
1062
890
  inputSchema: {
1063
- branch: z.string().optional().describe("Restrict to one branch id (default: all branches)"),
1064
- limit: z.number().optional().describe("Max rows, newest first (default 20, max 100)")
891
+ branch: z4.string().optional().describe("Restrict to one branch id (default: all branches)"),
892
+ limit: z4.number().optional().describe("Max rows, newest first (default 20, max 100)")
1065
893
  }
1066
894
  },
1067
895
  ({ branch, limit }) => guarded(async () => ({
@@ -1086,112 +914,31 @@ function createGraftMcp(options) {
1086
914
  }))
1087
915
  }))
1088
916
  );
1089
- server.registerTool(
1090
- "list_approvals",
1091
- {
1092
- title: "List pending approvals",
1093
- description: "Pending human-gated approvals. Decide with decide_approval, Studio Approve/Deny, or `graft approve` / `graft deny`. Same data as GET /api/studio/v1/approvals.",
1094
- inputSchema: {}
1095
- },
1096
- () => guarded(async () => ({
1097
- approvals: (await listPendingApprovals(
1098
- requireDb(
1099
- "list_approvals",
1100
- "Approvals gate destructive operations on operational data, which a static project does not have."
1101
- )
1102
- )).map((row) => ({
1103
- id: row.id,
1104
- branchId: row.branchId,
1105
- functionName: row.functionName,
1106
- input: row.input,
1107
- requestedByKind: row.requestedByKind,
1108
- requestedById: row.requestedById,
1109
- correlationId: row.correlationId,
1110
- createdAt: row.createdAt.toISOString()
1111
- }))
1112
- }))
1113
- );
1114
- server.registerTool(
1115
- "decide_approval",
1116
- {
1117
- title: "Approve or deny a pending approval",
1118
- description: "Record a human decision on a pending approval (same as Studio Approve/Deny and `graft approve` / `graft deny`). Requires an owner DB role that can UPDATE approvals. The requester cannot decide their own approval.",
1119
- inputSchema: {
1120
- id: z.string().describe("Pending approval id from list_approvals"),
1121
- decision: z.enum(["approved", "denied"]).describe("approved or denied"),
1122
- decidedBy: z.string().optional().describe("Operator identity stamp (defaults to mcp-operator)")
1123
- }
1124
- },
1125
- ({ id, decision, decidedBy }) => guarded(async () => {
1126
- const row = await decideApproval(
1127
- requireDb(
1128
- "decide_approval",
1129
- "Approvals gate destructive operations on operational data, which a static project does not have."
1130
- ),
1131
- id,
1132
- decision,
1133
- decidedBy?.trim() || "mcp-operator"
1134
- );
1135
- if (!row) {
1136
- throw new GraftError2({
1137
- code: "APPROVAL_INVALID",
1138
- message: `No PENDING approval "${id}" exists \u2014 it may already be decided, consumed, or mistyped.`,
1139
- fix: "Call list_approvals and use a pending id.",
1140
- details: { id }
1141
- });
1142
- }
1143
- return {
1144
- id: row.id,
1145
- status: row.status,
1146
- decidedBy: row.decidedBy,
1147
- functionName: row.functionName
1148
- };
1149
- })
1150
- );
1151
- server.registerTool(
1152
- "explain_error",
1153
- {
1154
- title: "Explain a Graft error",
1155
- description: "Given a GraftError code or its JSON, explain what it means, its typical causes, and how to recover. Use whenever a tool call or compile fails.",
1156
- inputSchema: {
1157
- code: z.string().optional().describe("An error code, e.g. SCHEMA_VALIDATION_FAILED"),
1158
- error: z.string().optional().describe("A full GraftError JSON string, if you have one")
1159
- }
1160
- },
1161
- ({ code, error }) => guarded(() => {
1162
- let parsed;
1163
- if (error) {
1164
- try {
1165
- parsed = JSON.parse(error);
1166
- } catch {
1167
- }
1168
- }
1169
- const effective = code ?? parsed?.error;
1170
- if (!effective) {
1171
- return {
1172
- knownCodes: Object.keys(ERROR_KNOWLEDGE),
1173
- hint: "Pass `code` or the GraftError JSON as `error`."
1174
- };
1175
- }
1176
- const explanation = explainCode(effective);
1177
- if (!explanation) {
1178
- return {
1179
- code: effective,
1180
- known: false,
1181
- knownCodes: Object.keys(ERROR_KNOWLEDGE),
1182
- hint: "Not a Graft error code. If this came from another system, resolve it there."
1183
- };
1184
- }
1185
- return {
1186
- ...explanation,
1187
- // The specific fix from the actual error beats the general recovery advice.
1188
- specificFix: parsed?.fix,
1189
- message: parsed?.message
1190
- };
1191
- })
1192
- );
1193
- return server;
1194
- }
917
+ };
918
+
919
+ // src/tools/content.ts
920
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
921
+ import {
922
+ composeDocument,
923
+ parseDocument as parseDocument2,
924
+ readCollectionDocs,
925
+ resolveContained as resolveContained2,
926
+ SLUG_RE,
927
+ writeDocumentFile
928
+ } from "@usegraft/compiler";
929
+ import { GraftError as GraftError5 } from "@usegraft/contracts";
930
+ import { assertSafeMdx } from "@usegraft/mdx-safety";
931
+ import { assertSearchQuery, scopeChain } from "@usegraft/db";
932
+ import { z as z5 } from "zod";
933
+
934
+ // src/tool-helpers.ts
935
+ import { existsSync, readdirSync, readFileSync as readFileSync2, statSync } from "fs";
936
+ import { join } from "path";
937
+ import { parseDocument } from "@usegraft/compiler";
938
+ import {
939
+ GraftError as GraftError4
940
+ } from "@usegraft/contracts";
941
+ import { APPROVAL_HEADER } from "@usegraft/core";
1195
942
  async function invokeFunction(handler, name, input, identity) {
1196
943
  const headers = new Headers({ "content-type": "application/json" });
1197
944
  if (identity.credential) {
@@ -1217,6 +964,26 @@ async function invokeFunction(handler, name, input, identity) {
1217
964
  const data = body !== null && typeof body === "object" && "data" in body ? body.data : body;
1218
965
  return { data, correlationId, status: response.status };
1219
966
  }
967
+ function filedApprovalId(error) {
968
+ if (!(error instanceof GraftError4) || error.code !== "DESTRUCTIVE_OP_REQUIRES_APPROVAL") {
969
+ return void 0;
970
+ }
971
+ const id = error.details?.approvalId;
972
+ return typeof id === "string" ? id : void 0;
973
+ }
974
+ async function invokeFunctionWithApproval(handler, name, input, identity, elicit) {
975
+ try {
976
+ return await invokeFunction(handler, name, input, identity);
977
+ } catch (error) {
978
+ const approvalId = filedApprovalId(error);
979
+ if (approvalId === void 0 || elicit === void 0 || identity.approval !== void 0) {
980
+ throw error;
981
+ }
982
+ const approved = await elicit({ approvalId, functionName: name, input });
983
+ if (approved === void 0) throw error;
984
+ return invokeFunction(handler, name, input, { ...identity, approval: approved });
985
+ }
986
+ }
1220
987
  var ASSET_FIELD_HINT = "Asset reference: the value is an object { key, alt? }. Upload the file with the put_asset tool first \u2014 its response includes the exact snippet to use here.";
1221
988
  function teachAssetFields(fieldDescriptor) {
1222
989
  const taught = {
@@ -1237,7 +1004,7 @@ function graftErrorFromBody(body, correlationId) {
1237
1004
  if (body !== null && typeof body === "object") {
1238
1005
  const json = body;
1239
1006
  if (typeof json.error === "string" && typeof json.message === "string") {
1240
- return new GraftError2({
1007
+ return new GraftError4({
1241
1008
  code: json.error,
1242
1009
  message: json.message,
1243
1010
  fix: toMcpFix(json.fix),
@@ -1248,7 +1015,7 @@ function graftErrorFromBody(body, correlationId) {
1248
1015
  });
1249
1016
  }
1250
1017
  }
1251
- return new GraftError2({
1018
+ return new GraftError4({
1252
1019
  code: "FUNCTION_EXECUTION_FAILED",
1253
1020
  message: "Function invocation failed with a non-GraftError response.",
1254
1021
  fix: "Inspect the server logs; retry with list_functions / describe_function to confirm the name and input shape.",
@@ -1256,23 +1023,23 @@ function graftErrorFromBody(body, correlationId) {
1256
1023
  });
1257
1024
  }
1258
1025
  function assertSlugFree(contentDir, collectionName, collection, slug, targetSourcePath) {
1259
- const dir = join2(contentDir, collectionName);
1260
- if (!existsSync2(dir) || !statSync2(dir).isDirectory()) return;
1261
- for (const name of readdirSync2(dir, { recursive: true, encoding: "utf8" })) {
1026
+ const dir = join(contentDir, collectionName);
1027
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) return;
1028
+ for (const name of readdirSync(dir, { recursive: true, encoding: "utf8" })) {
1262
1029
  const normalized = name.split("\\").join("/");
1263
1030
  const sourcePath = `${collectionName}/${normalized}`;
1264
- const full = join2(dir, name);
1265
- if (sourcePath === targetSourcePath || !/\.mdx?$/.test(name) || statSync2(full).isDirectory()) {
1031
+ const full = join(dir, name);
1032
+ if (sourcePath === targetSourcePath || !/\.mdx?$/.test(name) || statSync(full).isDirectory()) {
1266
1033
  continue;
1267
1034
  }
1268
1035
  let existingSlug;
1269
1036
  try {
1270
- existingSlug = parseDocument2(readFileSync2(full, "utf8"), collection, sourcePath).slug;
1037
+ existingSlug = parseDocument(readFileSync2(full, "utf8"), collection, sourcePath).slug;
1271
1038
  } catch {
1272
1039
  continue;
1273
1040
  }
1274
1041
  if (existingSlug === slug) {
1275
- throw new GraftError2({
1042
+ throw new GraftError4({
1276
1043
  code: "SLUG_NOT_UNIQUE",
1277
1044
  message: `Slug "${slug}" in collection "${collectionName}" is already used by ${sourcePath}`,
1278
1045
  fix: `Update that document instead (write_content with slug "${slug}" targets ${targetSourcePath}, but ${sourcePath} owns the slug via frontmatter), or pick a different slug.`,
@@ -1282,47 +1049,1337 @@ function assertSlugFree(contentDir, collectionName, collection, slug, targetSour
1282
1049
  }
1283
1050
  }
1284
1051
 
1285
- // src/http.ts
1286
- import { GraftError as GraftError3 } from "@usegraft/contracts";
1287
- import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
1288
- function jsonRpcError(status, code, message, headers) {
1289
- return Response.json({ jsonrpc: "2.0", error: { code, message }, id: null }, { status, headers });
1052
+ // src/tools/resource-uri.ts
1053
+ var documentUri = (branchId, collection, slug) => `graft://${branchId}/${collection}/${slug}`;
1054
+ var schemaUri = (branchId) => `graft://${branchId}/schema`;
1055
+ var documentUriTemplate = (branchId) => `graft://${branchId}/{collection}/{slug}`;
1056
+ var documentLink = (branchId, collection, slug) => ({
1057
+ type: "resource_link",
1058
+ uri: documentUri(branchId, collection, slug),
1059
+ name: `${collection}/${slug}`,
1060
+ mimeType: "text/markdown"
1061
+ });
1062
+
1063
+ // src/tools/content.ts
1064
+ function assertSlugShape(slug, collection) {
1065
+ if (SLUG_RE.test(slug)) return;
1066
+ throw new GraftError5({
1067
+ code: "INVALID_SLUG",
1068
+ message: `Slug "${slug}" is not URL-safe.`,
1069
+ fix: 'Slugs must be kebab-case: lowercase letters, digits, and single hyphens (e.g. "getting-started"). A slug names a file inside the collection directory, so it cannot contain "/", ".." or a drive letter.',
1070
+ details: { slug, collection, pattern: SLUG_RE.source }
1071
+ });
1290
1072
  }
1291
- function createGraftMcpHandler(options) {
1292
- const { actor: resolveActor, requireActor, ...serverOptions } = options;
1293
- return async (request) => {
1294
- if (request.method !== "POST") {
1295
- return jsonRpcError(405, -32e3, "Method not allowed: this server is stateless (POST only)", {
1296
- allow: "POST"
1297
- });
1298
- }
1299
- if (resolveActor) {
1300
- let actor;
1301
- try {
1302
- actor = await resolveActor(request);
1303
- } catch (err) {
1304
- const message = err instanceof GraftError3 ? `${err.message} ${err.fix ?? ""}`.trim() : "Unauthorized";
1305
- return jsonRpcError(401, -32001, message);
1073
+ var registerContentReadTools = (server, deps) => {
1074
+ const { branchId, collections, contentDir, getScope, searchIndex, staticIndexPath } = deps;
1075
+ server.registerTool(
1076
+ "list_content",
1077
+ {
1078
+ title: "List documents in a collection",
1079
+ outputSchema: listContentOutput,
1080
+ annotations: READS,
1081
+ description: "List every document in a collection, read from the authored MDX files (git is the source of truth). Returns slug, sourcePath, and frontmatter data.",
1082
+ inputSchema: {
1083
+ collection: z5.string().describe("Collection name, as returned by list_collections")
1306
1084
  }
1307
- if (requireActor && actor.kind === "anonymous") {
1308
- return jsonRpcError(
1309
- 401,
1310
- -32001,
1311
- "Unauthorized: this MCP endpoint requires authentication. Send `Authorization: Bearer <token>` from a trusted issuer."
1312
- );
1085
+ },
1086
+ ({ collection: name }) => guarded(
1087
+ () => {
1088
+ const collection = requireCollection(collections, name);
1089
+ const docs = readCollectionDocs(contentDir, name, collection);
1090
+ return {
1091
+ collection: name,
1092
+ documents: docs.map((doc) => ({
1093
+ slug: doc.slug,
1094
+ sourcePath: doc.sourcePath,
1095
+ data: doc.data
1096
+ }))
1097
+ };
1098
+ },
1099
+ (payload) => payload.documents.map((doc) => documentLink(branchId, name, doc.slug))
1100
+ )
1101
+ );
1102
+ server.registerTool(
1103
+ "get_content",
1104
+ {
1105
+ title: "Get one document",
1106
+ outputSchema: getContentOutput,
1107
+ annotations: READS,
1108
+ description: "Read a single document by collection + slug from the authored MDX files: validated frontmatter data, MDX body, and the file path to edit.",
1109
+ inputSchema: {
1110
+ collection: z5.string().describe("Collection name"),
1111
+ slug: z5.string().describe("Document slug (kebab-case)")
1112
+ }
1113
+ },
1114
+ ({ collection: name, slug }) => guarded(
1115
+ () => {
1116
+ const collection = requireCollection(collections, name);
1117
+ const doc = findDoc(contentDir, name, collection, slug);
1118
+ return {
1119
+ collection: name,
1120
+ slug: doc.slug,
1121
+ sourcePath: doc.sourcePath,
1122
+ data: doc.data,
1123
+ body: doc.body
1124
+ };
1125
+ },
1126
+ // The document's own address, so a client that reached it by search or
1127
+ // by guesswork can pin it as context without constructing the URI.
1128
+ (payload) => [documentLink(branchId, name, payload.slug)]
1129
+ )
1130
+ );
1131
+ server.registerTool(
1132
+ "search_content",
1133
+ {
1134
+ title: "Full-text search across content",
1135
+ outputSchema: searchContentOutput,
1136
+ annotations: READS,
1137
+ description: 'Search authored content by words, "quoted phrases", `or`, and -exclusions (websearch syntax). Searches the branch\'s effective content in the compiled Postgres index \u2014 on a preview branch that includes documents inherited from parent branches, with branch overrides winning \u2014 so results are as fresh as the last compile (write_content compiles automatically); every hit carries the sourcePath of the file to edit. Ranking weights slug matches over frontmatter over body.',
1138
+ inputSchema: {
1139
+ query: z5.string().describe('What to find, e.g. pricing "free tier" -enterprise'),
1140
+ collection: z5.string().optional().describe("Restrict to one collection (default: all registered collections)"),
1141
+ limit: z5.number().optional().describe("Max hits, best-ranked first (default 20)")
1313
1142
  }
1314
- } else if (requireActor) {
1315
- return jsonRpcError(
1316
- 401,
1317
- -32001,
1318
- "Unauthorized: requireActor is set but no actor resolver is configured \u2014 the server cannot authenticate anyone."
1143
+ },
1144
+ ({ query, collection: name, limit }) => guarded(
1145
+ async () => {
1146
+ if (name !== void 0) requireCollection(collections, name);
1147
+ assertSearchQuery(query);
1148
+ const collectionNames = name === void 0 ? Object.keys(collections) : [name];
1149
+ const chain = staticIndexPath === void 0 ? scopeChain(await getScope()) : [branchId];
1150
+ const hits = await searchIndex({ query, chain, collections: collectionNames, limit });
1151
+ return {
1152
+ branch: branchId,
1153
+ chain,
1154
+ query,
1155
+ hits: hits.map(({ row, rank, snippet }) => ({
1156
+ collection: row.collection,
1157
+ slug: row.slug,
1158
+ sourcePath: row.sourcePath,
1159
+ rank,
1160
+ snippet,
1161
+ data: row.data
1162
+ }))
1163
+ };
1164
+ },
1165
+ // A hit already knows its collection and slug; the link saves the
1166
+ // client a get_content round trip to reach the document itself.
1167
+ (payload) => payload.hits.map((hit) => documentLink(branchId, hit.collection, hit.slug))
1168
+ )
1169
+ );
1170
+ };
1171
+ var registerContentWriteTools = (server, deps) => {
1172
+ const {
1173
+ branchId,
1174
+ collections,
1175
+ contentDir,
1176
+ elicitApproval,
1177
+ functions,
1178
+ getDeleteHandler,
1179
+ options,
1180
+ projectContent,
1181
+ requireScope
1182
+ } = deps;
1183
+ server.registerTool(
1184
+ "write_content",
1185
+ {
1186
+ title: "Write a document (create or update)",
1187
+ outputSchema: writeContentOutput,
1188
+ annotations: WRITES,
1189
+ description: "Author or update a document: validates the data against the collection schema, writes <contentDir>/<collection>/<slug>.mdx, and compiles the content tree into the database. Returns exactly what changed. Git is the version history: commit the file afterwards if you have the server's checkout; remote callers can't and needn't \u2014 the checkout's operator owns the commit.",
1190
+ inputSchema: {
1191
+ collection: z5.string().describe("Collection name"),
1192
+ slug: z5.string().describe("Document slug \u2014 kebab-case; becomes the filename and the URL segment"),
1193
+ data: z5.record(z5.string(), z5.unknown()).describe("Frontmatter data; must satisfy the collection schema (see describe_schema)"),
1194
+ body: z5.string().optional().describe("MDX body (markdown). Defaults to empty.")
1195
+ }
1196
+ },
1197
+ ({ collection: name, slug, data, body }) => guarded(
1198
+ async () => {
1199
+ requireScope("write_content", "content:write");
1200
+ const collection = requireCollection(collections, name);
1201
+ if (collection.authority === "db-authoritative") {
1202
+ throw new GraftError5({
1203
+ code: "AUTHORITY_MISMATCH",
1204
+ message: `Collection "${name}" is db-authoritative \u2014 its records live in Postgres, not as MDX files.`,
1205
+ fix: `Write this data through the collection's function endpoint (POST /api/fn/<name>, see llms.txt) instead of write_content. write_content is only for file-authoritative collections.`,
1206
+ details: { collection: name, authority: collection.authority }
1207
+ });
1208
+ }
1209
+ const frontmatterSlug = data.slug;
1210
+ if (frontmatterSlug !== void 0 && frontmatterSlug !== slug) {
1211
+ throw new GraftError5({
1212
+ code: "INVALID_SLUG",
1213
+ message: `data.slug ("${String(frontmatterSlug)}") conflicts with the slug argument ("${slug}")`,
1214
+ fix: "Omit `slug` from data \u2014 the slug argument names the file and the document.",
1215
+ details: { slug, frontmatterSlug }
1216
+ });
1217
+ }
1218
+ assertSlugShape(slug, name);
1219
+ const sourcePath = `${name}/${slug}.mdx`;
1220
+ const fullPath = resolveContained2(contentDir, sourcePath, {
1221
+ label: "document path"
1222
+ });
1223
+ assertSafeMdx(body ?? "", { label: `${name}/${slug}` });
1224
+ const existingRaw = existsSync2(fullPath) ? readFileSync3(fullPath, "utf8") : void 0;
1225
+ const raw = composeDocument(existingRaw, data, body ?? "");
1226
+ parseDocument2(raw, collection, sourcePath);
1227
+ assertSlugFree(contentDir, name, collection, slug, sourcePath);
1228
+ writeDocumentFile(contentDir, sourcePath, raw);
1229
+ const result = await projectContent();
1230
+ return {
1231
+ written: sourcePath,
1232
+ branch: branchId,
1233
+ gitSha: result.gitSha,
1234
+ changes: result.changes
1235
+ };
1236
+ },
1237
+ // Where the thing it just wrote now lives.
1238
+ () => [documentLink(branchId, name, slug)]
1239
+ )
1240
+ );
1241
+ server.registerTool(
1242
+ "delete_content",
1243
+ {
1244
+ title: "Delete a document (human-gated)",
1245
+ annotations: DESTROYS,
1246
+ description: "Delete an authored document: removes <contentDir>/<collection>/<slug>.mdx and compiles, so the index soft-deletes it. DESTRUCTIVE and always human-gated \u2014 the first call files an approval and fails with its id; a human decides with `graft approve <id>` (or deny); then retry the SAME collection+slug with `approval: <id>` (the MCP form of the x-graft-approval header). Approvals are one-shot and bound to that exact input. On a server that has opted into elicited approvals AND a client that supports elicitation, you may instead be asked to confirm in-band and the call completes in one step \u2014 the gate is identical, only the way the human is reached differs, so do not treat either outcome as unusual. Git is the version history: commit the deletion afterwards if you have the server's checkout; remote callers can't and needn't \u2014 the checkout's operator owns the commit.",
1247
+ inputSchema: {
1248
+ collection: z5.string().describe("Collection name"),
1249
+ slug: z5.string().describe("Document slug to delete"),
1250
+ approval: z5.string().optional().describe(
1251
+ "Approval id from a prior DESTRUCTIVE_OP_REQUIRES_APPROVAL response, after a human ran `graft approve <id>`."
1252
+ )
1253
+ }
1254
+ },
1255
+ ({ collection: name, slug, approval }) => guarded(async () => {
1256
+ requireScope("delete_content", "content:write");
1257
+ const collection = requireCollection(collections, name);
1258
+ if (collection.authority === "db-authoritative") {
1259
+ throw new GraftError5({
1260
+ code: "AUTHORITY_MISMATCH",
1261
+ message: `Collection "${name}" is db-authoritative \u2014 its records live in Postgres, not as MDX files.`,
1262
+ fix: "Delete records through the collection's typed functions (a destructive defineFunction over deleteRecord \u2014 see list_functions), not delete_content. delete_content is only for file-authoritative collections.",
1263
+ details: { collection: name, authority: collection.authority }
1264
+ });
1265
+ }
1266
+ assertSlugShape(slug, name);
1267
+ findDoc(contentDir, name, collection, slug);
1268
+ const { data, correlationId } = await invokeFunctionWithApproval(
1269
+ getDeleteHandler(),
1270
+ "delete_content",
1271
+ { collection: name, slug },
1272
+ { credential: options.defaultAuthorization, approval },
1273
+ elicitApproval
1319
1274
  );
1320
- }
1321
- const server = createGraftMcp({
1322
- ...serverOptions,
1323
- actor: resolveActor,
1324
- defaultAuthorization: request.headers.get("authorization") ?? serverOptions.defaultAuthorization
1325
- });
1275
+ return { ...data, correlationId };
1276
+ })
1277
+ );
1278
+ };
1279
+
1280
+ // src/tools/errors.ts
1281
+ import { z as z6 } from "zod";
1282
+ var registerErrorTools = (server, deps) => {
1283
+ void deps;
1284
+ server.registerTool(
1285
+ "explain_error",
1286
+ {
1287
+ title: "Explain a Graft error",
1288
+ outputSchema: explainErrorOutput,
1289
+ annotations: READS,
1290
+ description: "Given a GraftError code or its JSON, explain what it means, its typical causes, and how to recover. Use whenever a tool call or compile fails.",
1291
+ inputSchema: {
1292
+ code: z6.string().optional().describe("An error code, e.g. SCHEMA_VALIDATION_FAILED"),
1293
+ error: z6.string().optional().describe("A full GraftError JSON string, if you have one")
1294
+ }
1295
+ },
1296
+ ({ code, error }) => guarded(() => {
1297
+ let parsed;
1298
+ if (error) {
1299
+ try {
1300
+ parsed = JSON.parse(error);
1301
+ } catch {
1302
+ }
1303
+ }
1304
+ const effective = code ?? parsed?.error;
1305
+ if (!effective) {
1306
+ return {
1307
+ knownCodes: Object.keys(ERROR_KNOWLEDGE),
1308
+ hint: "Pass `code` or the GraftError JSON as `error`."
1309
+ };
1310
+ }
1311
+ const explanation = explainCode(effective);
1312
+ if (!explanation) {
1313
+ return {
1314
+ code: effective,
1315
+ known: false,
1316
+ knownCodes: Object.keys(ERROR_KNOWLEDGE),
1317
+ hint: "Not a Graft error code. If this came from another system, resolve it there."
1318
+ };
1319
+ }
1320
+ return {
1321
+ ...explanation,
1322
+ // The specific fix from the actual error beats the general recovery advice.
1323
+ specificFix: parsed?.fix,
1324
+ message: parsed?.message
1325
+ };
1326
+ })
1327
+ );
1328
+ };
1329
+
1330
+ // src/tools/packages.ts
1331
+ import { z as z7 } from "zod";
1332
+
1333
+ // src/packages.ts
1334
+ var PACKAGE_KNOWLEDGE = {
1335
+ "@usegraft/cli": {
1336
+ name: "@usegraft/cli",
1337
+ role: "The `graft` command: init, compile, dev, serve, migrate, merge, add.",
1338
+ when: "Always. It scaffolds the project and compiles content into the index.",
1339
+ tier: "either",
1340
+ direct: true
1341
+ },
1342
+ "@usegraft/sdk-next": {
1343
+ name: "@usegraft/sdk-next",
1344
+ role: "Next.js adapter: typed reads in Server Components, plus route handlers.",
1345
+ when: "You are on Next.js (App Router).",
1346
+ tier: "either",
1347
+ framework: "next",
1348
+ direct: true
1349
+ },
1350
+ "@usegraft/sdk-astro": {
1351
+ name: "@usegraft/sdk-astro",
1352
+ role: "Astro adapter: typed reads in components, plus `graftRoute`.",
1353
+ when: "You are on Astro.",
1354
+ tier: "either",
1355
+ framework: "astro",
1356
+ direct: true
1357
+ },
1358
+ "@usegraft/sdk-sveltekit": {
1359
+ name: "@usegraft/sdk-sveltekit",
1360
+ role: "SvelteKit adapter: typed reads in load functions, plus route handlers.",
1361
+ when: "You are on SvelteKit.",
1362
+ tier: "either",
1363
+ framework: "sveltekit",
1364
+ direct: true
1365
+ },
1366
+ "@usegraft/sdk-react-router": {
1367
+ name: "@usegraft/sdk-react-router",
1368
+ role: "React Router adapter: typed reads in loaders and actions.",
1369
+ when: "You are on React Router in framework mode, which runs a server.",
1370
+ tier: "either",
1371
+ framework: "react-router",
1372
+ direct: true
1373
+ },
1374
+ "@usegraft/sdk-tanstack-start": {
1375
+ name: "@usegraft/sdk-tanstack-start",
1376
+ role: "TanStack Start adapter: typed reads in server functions and loaders.",
1377
+ when: "You are on TanStack Start.",
1378
+ tier: "either",
1379
+ framework: "tanstack-start",
1380
+ direct: true
1381
+ },
1382
+ "@usegraft/sdk-react": {
1383
+ name: "@usegraft/sdk-react",
1384
+ role: "Browser client: typed reads over the content API, with a provider and hooks.",
1385
+ when: "You need reads in the browser \u2014 a search box, an editor preview, a widget on an already-rendered page. No database reaches this package.",
1386
+ tier: "postgres",
1387
+ framework: "react",
1388
+ direct: true
1389
+ },
1390
+ "@usegraft/sdk-core": {
1391
+ name: "@usegraft/sdk-core",
1392
+ role: "The framework-agnostic read client the adapters are built on.",
1393
+ when: "You are on a framework with no adapter, or you want the client directly.",
1394
+ tier: "either",
1395
+ direct: true
1396
+ },
1397
+ "@usegraft/content-api": {
1398
+ name: "@usegraft/content-api",
1399
+ role: "The HTTP content API: a handler to mount, and a reader to consume it.",
1400
+ when: "Something reads content over HTTP rather than in-process \u2014 the browser client, or another service.",
1401
+ tier: "either",
1402
+ direct: true
1403
+ },
1404
+ "@usegraft/mcp": {
1405
+ name: "@usegraft/mcp",
1406
+ role: "The MCP server: authoring, schema introspection, functions and approvals as tools.",
1407
+ when: "You want an agent to read and write content. `graft mcp` and `graft serve` both mount it.",
1408
+ tier: "either",
1409
+ direct: true
1410
+ },
1411
+ "@usegraft/studio": {
1412
+ name: "@usegraft/studio",
1413
+ role: "The editing UI: documents, compiles, commits, branches and approvals.",
1414
+ when: "A human needs to edit without touching the repository.",
1415
+ tier: "postgres",
1416
+ direct: true
1417
+ },
1418
+ "@usegraft/core": {
1419
+ name: "@usegraft/core",
1420
+ role: "defineCollection, defineFunction, the functions handler, and the field primitives.",
1421
+ when: "Always \u2014 it is what graft.config.ts imports to declare a schema.",
1422
+ tier: "either",
1423
+ direct: true
1424
+ },
1425
+ "@usegraft/db": {
1426
+ name: "@usegraft/db",
1427
+ role: "The Postgres schema and Drizzle handle for operational data.",
1428
+ when: "You are on the Postgres tier. Never reaches a browser.",
1429
+ tier: "postgres",
1430
+ direct: true
1431
+ },
1432
+ "@usegraft/auth": {
1433
+ name: "@usegraft/auth",
1434
+ role: "Actor resolution: bearer tokens, OIDC issuers and scopes.",
1435
+ when: "You are authenticating callers to functions, MCP or Studio.",
1436
+ tier: "postgres",
1437
+ direct: true
1438
+ },
1439
+ "@usegraft/assets": {
1440
+ name: "@usegraft/assets",
1441
+ role: "The S3-compatible asset store behind `graft asset put` and `put_asset`.",
1442
+ when: "Documents reference images or files.",
1443
+ tier: "either",
1444
+ direct: true
1445
+ },
1446
+ "@usegraft/compiler": {
1447
+ name: "@usegraft/compiler",
1448
+ role: "Reads authored MDX and projects it into the content index.",
1449
+ when: "Pulled in by the CLI. Install directly only to compile from your own code.",
1450
+ tier: "either",
1451
+ direct: false
1452
+ },
1453
+ "@usegraft/contracts": {
1454
+ name: "@usegraft/contracts",
1455
+ role: "Shared types, error codes and the content-index interface.",
1456
+ when: "A dependency of everything. Install directly only to implement your own index reader.",
1457
+ tier: "either",
1458
+ direct: false
1459
+ },
1460
+ "@usegraft/mdx-safety": {
1461
+ name: "@usegraft/mdx-safety",
1462
+ role: "Refuses executable MDX in authored bodies, per the project's mdxTrust.",
1463
+ when: "Pulled in by the compiler and MCP. Install directly only to check MDX yourself.",
1464
+ tier: "either",
1465
+ direct: false
1466
+ },
1467
+ "@usegraft/content-migrations": {
1468
+ name: "@usegraft/content-migrations",
1469
+ role: "Migrations for authored documents, as opposed to database rows.",
1470
+ when: "A schema change needs existing MDX rewritten.",
1471
+ tier: "either",
1472
+ direct: true
1473
+ },
1474
+ "@usegraft/tokens": {
1475
+ name: "@usegraft/tokens",
1476
+ role: "Graft's design tokens as plain CSS custom properties.",
1477
+ when: "You are restyling Studio, or want a UI that matches it. One CSS import, no JavaScript.",
1478
+ tier: "either",
1479
+ direct: true
1480
+ },
1481
+ "@usegraft/registry": {
1482
+ name: "@usegraft/registry",
1483
+ role: "The owned primitives `graft add` copies into a project.",
1484
+ when: "Pulled in by the CLI. Browse the items with list_registry.",
1485
+ tier: "either",
1486
+ direct: false
1487
+ }
1488
+ };
1489
+ function allPackages() {
1490
+ return Object.values(PACKAGE_KNOWLEDGE);
1491
+ }
1492
+
1493
+ // src/tools/packages.ts
1494
+ var registerPackageTools = (server, deps) => {
1495
+ void deps;
1496
+ server.registerTool(
1497
+ "list_packages",
1498
+ {
1499
+ title: "List Graft packages",
1500
+ outputSchema: listPackagesOutput,
1501
+ annotations: READS,
1502
+ description: "Which @usegraft/* package to install for a given framework or job: what each one is, when you need it, and which tier it requires. Filter by `framework` when the user is on a known one, or by `tier`. Use this before suggesting an install; use list_registry for copy-in primitives instead.",
1503
+ inputSchema: {
1504
+ framework: z7.enum(["next", "astro", "sveltekit", "react-router", "tanstack-start", "react"]).optional().describe("Only the adapter for this framework, plus the packages every project needs"),
1505
+ tier: z7.enum(["static", "postgres"]).optional().describe("Only packages usable on this tier"),
1506
+ includeIndirect: z7.boolean().optional().describe("Include packages pulled in as dependencies rather than installed directly")
1507
+ }
1508
+ },
1509
+ ({ framework, tier, includeIndirect }) => guarded(() => {
1510
+ let packages = allPackages();
1511
+ if (!includeIndirect) packages = packages.filter((p) => p.direct);
1512
+ if (framework) {
1513
+ packages = packages.filter((p) => p.framework === void 0 || p.framework === framework);
1514
+ }
1515
+ if (tier) packages = packages.filter((p) => p.tier === "either" || p.tier === tier);
1516
+ return { packages: packages.sort((a, b) => a.name.localeCompare(b.name)) };
1517
+ })
1518
+ );
1519
+ };
1520
+
1521
+ // src/tools/functions.ts
1522
+ import { GraftError as GraftError6 } from "@usegraft/contracts";
1523
+ import { z as z8 } from "zod";
1524
+ var registerFunctionTools = (server, deps) => {
1525
+ const { elicitApproval, functions, functionsByName, getFunctionsHandler, options } = deps;
1526
+ server.registerTool(
1527
+ "run_function",
1528
+ {
1529
+ title: "Run a typed function",
1530
+ annotations: DESTROYS,
1531
+ description: "Invoke a defineFunction by name with a JSON input object. Same pipeline as POST /api/fn/<name>: Zod validation, access rules, rate limits, audit log, and the human gate for destructive ops. The server may already act with a configured identity (graft mcp uses GRAFT_DEV_TOKEN; over HTTP your connection's bearer is forwarded) \u2014 only pass authorization to override it. Pass approval after a human runs `graft approve <id>` for gated calls \u2014 or, where the server has opted into elicited approvals and your client supports elicitation, expect to be asked to confirm in-band and the call completes in one step. The gate is the same either way. Success returns { data, correlationId }; failures are GraftError JSON with a fix.",
1532
+ inputSchema: {
1533
+ name: z8.string().describe("Function name (defineFunction name, not the export key)"),
1534
+ input: z8.record(z8.string(), z8.unknown()).optional().describe("Input fields object; defaults to {}. See describe_function for the schema."),
1535
+ authorization: z8.string().optional().describe(
1536
+ "Bearer token override (with or without the 'Bearer ' prefix). Usually unnecessary \u2014 the server's configured identity applies when omitted."
1537
+ ),
1538
+ approval: z8.string().optional().describe(
1539
+ "Approval id from a prior DESTRUCTIVE_OP_REQUIRES_APPROVAL response (after `graft approve <id>`)."
1540
+ )
1541
+ }
1542
+ },
1543
+ ({ name, input, authorization, approval }) => guarded(async () => {
1544
+ if (functionsByName.size === 0) {
1545
+ throw new GraftError6({
1546
+ code: "FUNCTION_NOT_FOUND",
1547
+ message: "This MCP server has no functions registered.",
1548
+ fix: "Export `functions` from graft.config.ts (defineFunction results, often via mergePrimitives) and restart the MCP server / pass them to createGraftMcp({ functions }).",
1549
+ details: { requested: name, available: [] }
1550
+ });
1551
+ }
1552
+ if (!functionsByName.has(name)) {
1553
+ throw new GraftError6({
1554
+ code: "FUNCTION_NOT_FOUND",
1555
+ message: `No function named "${name}" is registered.`,
1556
+ fix: `Call list_functions and use one of: ${[...functionsByName.keys()].join(", ")}.`,
1557
+ details: { requested: name, available: [...functionsByName.keys()] }
1558
+ });
1559
+ }
1560
+ return invokeFunctionWithApproval(
1561
+ getFunctionsHandler(),
1562
+ name,
1563
+ input ?? {},
1564
+ { credential: authorization ?? options.defaultAuthorization, approval },
1565
+ elicitApproval
1566
+ );
1567
+ })
1568
+ );
1569
+ };
1570
+
1571
+ // src/tools/introspection.ts
1572
+ import { GraftError as GraftError7 } from "@usegraft/contracts";
1573
+ import { z as z9 } from "zod";
1574
+ var registerCollectionIntrospection = (server, deps) => {
1575
+ const { branchId, collections } = deps;
1576
+ server.registerTool(
1577
+ "list_collections",
1578
+ {
1579
+ title: "List collections",
1580
+ outputSchema: listCollectionsOutput,
1581
+ annotations: READS,
1582
+ description: "List every registered content collection (name, description, authority, field count). Start here to learn what kinds of content this project has.",
1583
+ inputSchema: {}
1584
+ },
1585
+ () => guarded(() => ({
1586
+ branch: branchId,
1587
+ collections: Object.values(collections).map((collection) => {
1588
+ const descriptor = collection.describe();
1589
+ return {
1590
+ name: descriptor.name,
1591
+ description: descriptor.description,
1592
+ authority: descriptor.authority,
1593
+ fields: descriptor.fields.length
1594
+ };
1595
+ })
1596
+ }))
1597
+ );
1598
+ };
1599
+ var registerFunctionIntrospection = (server, deps) => {
1600
+ const { branchId, collections, functions, functionsByName } = deps;
1601
+ server.registerTool(
1602
+ "describe_schema",
1603
+ {
1604
+ title: "Describe the content schema",
1605
+ outputSchema: describeSchemaOutput,
1606
+ annotations: READS,
1607
+ description: "Full schema introspection: every collection with its typed fields (name, type, optional, description), plus every registered function (kind, args, public/destructive). Documents also accept an optional kebab-case `slug` (defaults to the filename). Prefer list_functions / describe_function when you only need the function surface.",
1608
+ inputSchema: {}
1609
+ },
1610
+ () => guarded(() => {
1611
+ return {
1612
+ collections: Object.values(collections).map((collection) => {
1613
+ const descriptor = collection.describe();
1614
+ return { ...descriptor, fields: descriptor.fields.map(teachAssetFields) };
1615
+ }),
1616
+ functions: [...functionsByName.values()].map((fn) => fn.describe())
1617
+ };
1618
+ })
1619
+ );
1620
+ server.registerTool(
1621
+ "list_functions",
1622
+ {
1623
+ title: "List functions",
1624
+ outputSchema: listFunctionsOutput,
1625
+ annotations: READS,
1626
+ description: "List every registered typed function (name, kind, public, destructive, short description). Use describe_function for the full input schema, then run_function to invoke. Mutations reject anonymous callers unless public: true; destructive functions always require human approval (graft approve).",
1627
+ inputSchema: {}
1628
+ },
1629
+ () => guarded(() => ({
1630
+ branch: branchId,
1631
+ functions: [...functionsByName.values()].map((fn) => {
1632
+ const d = fn.describe();
1633
+ return {
1634
+ name: d.name,
1635
+ kind: d.kind,
1636
+ description: d.description,
1637
+ public: d.public,
1638
+ destructive: d.destructive,
1639
+ args: d.args.length
1640
+ };
1641
+ })
1642
+ }))
1643
+ );
1644
+ server.registerTool(
1645
+ "describe_function",
1646
+ {
1647
+ title: "Describe one function",
1648
+ outputSchema: describeFunctionOutput,
1649
+ annotations: READS,
1650
+ description: "Full introspection for one function: kind, args (name/type/optional/description), returns, public, destructive. Use this before run_function so the input object matches the schema.",
1651
+ inputSchema: {
1652
+ name: z9.string().describe("Function name as returned by list_functions")
1653
+ }
1654
+ },
1655
+ ({ name }) => guarded(() => {
1656
+ const fn = functionsByName.get(name);
1657
+ if (!fn) {
1658
+ throw new GraftError7({
1659
+ code: "FUNCTION_NOT_FOUND",
1660
+ message: `No function named "${name}" is registered.`,
1661
+ fix: `Call list_functions and use one of: ${[...functionsByName.keys()].join(", ") || "(none registered)"}.`,
1662
+ details: { requested: name, available: [...functionsByName.keys()] }
1663
+ });
1664
+ }
1665
+ return fn.describe();
1666
+ })
1667
+ );
1668
+ };
1669
+
1670
+ // src/approval-elicitation.ts
1671
+ import { GraftError as GraftError8 } from "@usegraft/contracts";
1672
+ import { decideApproval as decideApproval2 } from "@usegraft/db";
1673
+ var INPUT_PREVIEW_LIMIT = 1e3;
1674
+ function describeCall(functionName, input) {
1675
+ const rendered = JSON.stringify(input);
1676
+ if (rendered === void 0) return functionName;
1677
+ if (rendered.length <= INPUT_PREVIEW_LIMIT) return `${functionName} ${rendered}`;
1678
+ return [
1679
+ `${functionName} ${rendered.slice(0, INPUT_PREVIEW_LIMIT)}\u2026`,
1680
+ "",
1681
+ `\u26A0\uFE0F Input truncated (${rendered.length} characters). Run \`graft approvals\` to read it in full before deciding \u2014 the approval binds to the whole input, not the part shown here.`
1682
+ ].join("\n");
1683
+ }
1684
+ function createApprovalElicitor(options) {
1685
+ return async ({ approvalId, functionName, input }) => {
1686
+ if (options.server.server.getClientCapabilities()?.elicitation === void 0) {
1687
+ return void 0;
1688
+ }
1689
+ let result;
1690
+ try {
1691
+ result = await options.server.server.elicitInput({
1692
+ message: [
1693
+ `Approve this destructive call?`,
1694
+ "",
1695
+ describeCall(functionName, input),
1696
+ "",
1697
+ `It will be recorded as decided by ${options.decider.kind}:${options.decider.id}.`,
1698
+ "The approval is one-shot and bound to exactly this input."
1699
+ ].join("\n"),
1700
+ requestedSchema: {
1701
+ type: "object",
1702
+ properties: {
1703
+ approve: {
1704
+ type: "boolean",
1705
+ title: "Approve",
1706
+ description: `Allow ${functionName} to run once, with exactly this input.`
1707
+ }
1708
+ },
1709
+ required: ["approve"]
1710
+ }
1711
+ });
1712
+ } catch {
1713
+ return void 0;
1714
+ }
1715
+ if (result.action === "cancel") return void 0;
1716
+ const approved = result.action === "accept" && result.content?.approve === true;
1717
+ const row = await decideApproval2(
1718
+ options.db(),
1719
+ approvalId,
1720
+ approved ? "approved" : "denied",
1721
+ options.decider
1722
+ );
1723
+ if (!row) {
1724
+ throw new GraftError8({
1725
+ code: "APPROVAL_INVALID",
1726
+ message: `Approval "${approvalId}" was no longer pending when the decision arrived.`,
1727
+ fix: "Someone or something decided it while the prompt was open. Call the tool again to file a fresh approval.",
1728
+ details: { id: approvalId, function: functionName }
1729
+ });
1730
+ }
1731
+ return approved ? approvalId : void 0;
1732
+ };
1733
+ }
1734
+
1735
+ // src/tools/prompts.ts
1736
+ import { z as z10 } from "zod";
1737
+ import { completable } from "@modelcontextprotocol/sdk/server/completable.js";
1738
+ import { readCollectionDocs as readCollectionDocs2 } from "@usegraft/compiler";
1739
+ var say = (text) => ({
1740
+ messages: [{ role: "user", content: { type: "text", text } }]
1741
+ });
1742
+ var registerContentPrompts = (server, deps) => {
1743
+ const { branchId, collections, contentDir } = deps;
1744
+ const fileCollections = () => Object.values(collections).filter((collection) => collection.describe().authority !== "db-authoritative").map((collection) => collection.describe().name);
1745
+ const migrationSteps = (name) => {
1746
+ if (collections[name]?.describe().authority === "db-authoritative") {
1747
+ return [
1748
+ "This collection is db-authoritative: its records live in Postgres, not as MDX files. `graft compile` has nothing to say about them, and no file will fail to name the work.",
1749
+ "1. Edit the collection in graft.config.ts (or under graft/) to the new shape.",
1750
+ "2. Write `migrations/<seq>-<name>.ts` default-exporting defineDataMigration. It backfills data_records and writes its ledger row in ONE transaction, so a half-applied migration is not a state this can reach.",
1751
+ "3. `graft migrate` is a dry run. `graft migrate --apply` is the operator's consent, and is theirs to give \u2014 propose the command, do not assume it.",
1752
+ "4. Commit. The migration is a reviewable commit, which is the whole reason it is a file rather than a live edit."
1753
+ ];
1754
+ }
1755
+ return [
1756
+ "How migrations work here \u2014 they are code, not a console action:",
1757
+ "1. Edit the collection in graft.config.ts (or under graft/) to the new shape.",
1758
+ "2. `graft compile` now fails, once per document that no longer satisfies the schema. That is the point: the failure names every file to fix.",
1759
+ "3. Write `migrations/<seq>-<name>.ts` default-exporting defineContentMigration. It transforms old-shape frontmatter, validates every output against the NEW schema, and rewrites the files all-or-nothing.",
1760
+ "4. `graft migrate` is a dry run. `graft migrate --apply` is the operator's consent, and is theirs to give \u2014 propose the command, do not assume it.",
1761
+ "5. Compile again, then commit. The migration is a reviewable commit, which is the whole reason it is a file rather than a live edit."
1762
+ ];
1763
+ };
1764
+ const slugsIn = (name) => {
1765
+ const collection = collections[name];
1766
+ if (!collection) return [];
1767
+ try {
1768
+ return readCollectionDocs2(contentDir, name, collection).map((doc) => doc.slug);
1769
+ } catch {
1770
+ return [];
1771
+ }
1772
+ };
1773
+ const fieldLines = (name) => {
1774
+ const collection = collections[name];
1775
+ if (!collection) return "(unknown collection)";
1776
+ return collection.describe().fields.map((field2) => {
1777
+ const optional = field2.optional ? " (optional)" : " (required)";
1778
+ const description = field2.description ? ` \u2014 ${field2.description}` : "";
1779
+ return `- ${field2.name}: ${field2.type}${optional}${description}`;
1780
+ }).join("\n");
1781
+ };
1782
+ const collectionArg = completable(
1783
+ z10.string().describe("Collection name"),
1784
+ (value) => fileCollections().filter((name) => name.startsWith(value))
1785
+ );
1786
+ server.registerPrompt(
1787
+ "author-document",
1788
+ {
1789
+ title: "Author a document",
1790
+ description: "Write a new document into a collection, with that collection's actual field list already filled in.",
1791
+ argsSchema: {
1792
+ collection: collectionArg,
1793
+ topic: z10.string().describe("What the document should be about")
1794
+ }
1795
+ },
1796
+ ({ collection, topic }) => say(
1797
+ [
1798
+ `Author a new document in the "${collection}" collection of this Graft project. Topic: ${topic}`,
1799
+ "",
1800
+ "Frontmatter must satisfy these fields exactly:",
1801
+ fieldLines(collection),
1802
+ "",
1803
+ "How to do it:",
1804
+ "1. Choose a kebab-case slug. It becomes the filename and the URL segment.",
1805
+ "2. Call write_content with the collection, the slug, the frontmatter as `data`, and the MDX body.",
1806
+ "3. write_content validates against the schema and compiles in one step \u2014 a schema failure comes back with a fix, so read it and correct the data rather than retrying unchanged.",
1807
+ "",
1808
+ "Body rules: prose and Markdown. Components are allowed only with literal attributes \u2014 the MDX safety gate refuses `{expressions}` and `import`, because rendering evaluates them as JavaScript on the server.",
1809
+ "",
1810
+ "Git owns the version history. If you are working in the server's checkout, commit the file afterwards; if you are remote, the checkout's operator owns the commit and you do not need to."
1811
+ ].join("\n")
1812
+ )
1813
+ );
1814
+ server.registerPrompt(
1815
+ "revise-document",
1816
+ {
1817
+ title: "Revise a document",
1818
+ description: "Edit an existing document, with its current content and its address already attached.",
1819
+ argsSchema: {
1820
+ collection: collectionArg,
1821
+ slug: completable(z10.string().describe("Document slug"), (value, context) => {
1822
+ const chosen = context?.arguments?.collection;
1823
+ const candidates = chosen ? slugsIn(chosen) : fileCollections().flatMap(slugsIn);
1824
+ return candidates.filter((slug) => slug.startsWith(value));
1825
+ }),
1826
+ goal: z10.string().describe("What should change")
1827
+ }
1828
+ },
1829
+ ({ collection, slug, goal }) => say(
1830
+ [
1831
+ `Revise "${collection}/${slug}" in this Graft project. Goal: ${goal}`,
1832
+ "",
1833
+ `Read it first: ${documentUri(branchId, collection, slug)} (or get_content).`,
1834
+ "",
1835
+ "Frontmatter fields for this collection:",
1836
+ fieldLines(collection),
1837
+ "",
1838
+ "Then call write_content with the same collection and slug. Send the full frontmatter and body, not a patch \u2014 write_content replaces the document.",
1839
+ "",
1840
+ "Change only what the goal asks for. Frontmatter keys you are not changing must come back byte-identical: the writer preserves untouched lines, and a reformatted file is a diff the author has to review for nothing.",
1841
+ "",
1842
+ "If that write comes back SLUG_NOT_UNIQUE, the document is not where write_content would put it \u2014 it is a .md file, or it lives under a different filename and claims this slug in its frontmatter. write_content always targets <collection>/<slug>.mdx, so it cannot edit that file in place. The error names the path that owns the slug; report it rather than writing a second document with the same slug."
1843
+ ].join("\n")
1844
+ )
1845
+ );
1846
+ server.registerPrompt(
1847
+ "fix-error",
1848
+ {
1849
+ title: "Recover from a Graft error",
1850
+ description: "Turn a GraftError into the next action, with this build's recovery text already resolved.",
1851
+ argsSchema: {
1852
+ code: completable(
1853
+ z10.string().describe("The error code, e.g. SCHEMA_VALIDATION_FAILED"),
1854
+ (value) => Object.keys(ERROR_KNOWLEDGE).filter((code) => code.startsWith(value.toUpperCase()))
1855
+ )
1856
+ }
1857
+ },
1858
+ ({ code }) => {
1859
+ const explanation = explainCode(code);
1860
+ if (!explanation) {
1861
+ return say(
1862
+ [
1863
+ `"${code}" is not a Graft error code.`,
1864
+ "",
1865
+ `Call explain_error with no arguments to list the ${Object.keys(ERROR_KNOWLEDGE).length} codes this build knows, or pass the full GraftError JSON as \`error\`.`,
1866
+ "If it came from another system, resolve it there \u2014 Graft has nothing to say about it."
1867
+ ].join("\n")
1868
+ );
1869
+ }
1870
+ return say(
1871
+ [
1872
+ `Recover from ${code} in this Graft project.`,
1873
+ "",
1874
+ `What it means: ${explanation.meaning}`,
1875
+ "",
1876
+ "Usually because:",
1877
+ ...explanation.typicalCauses.map((cause) => `- ${cause}`),
1878
+ "",
1879
+ `How to recover: ${explanation.howToRecover}`,
1880
+ "",
1881
+ "The failing call's own `fix` is more specific than this page and beats it wherever the two differ \u2014 this is the general lesson behind the code, that was the advice for the one failure."
1882
+ ].join("\n")
1883
+ );
1884
+ }
1885
+ );
1886
+ server.registerPrompt(
1887
+ "plan-migration",
1888
+ {
1889
+ title: "Plan a schema migration",
1890
+ description: "Change a collection's shape without breaking the documents already written against it.",
1891
+ argsSchema: {
1892
+ collection: collectionArg,
1893
+ change: z10.string().describe("The schema change, e.g. add a required `description`")
1894
+ }
1895
+ },
1896
+ ({ collection, change }) => say(
1897
+ [
1898
+ `Plan a migration for the "${collection}" collection of this Graft project. Change: ${change}`,
1899
+ "",
1900
+ "Current fields:",
1901
+ fieldLines(collection),
1902
+ "",
1903
+ ...migrationSteps(collection)
1904
+ ].join("\n")
1905
+ )
1906
+ );
1907
+ };
1908
+
1909
+ // src/tools/registry.ts
1910
+ import { describeItem, listItems, loadItem } from "@usegraft/registry";
1911
+ import { z as z11 } from "zod";
1912
+ var registerRegistryTools = (server, deps) => {
1913
+ const { options } = deps;
1914
+ server.registerTool(
1915
+ "list_registry",
1916
+ {
1917
+ title: "List registry items",
1918
+ outputSchema: listRegistryOutput,
1919
+ annotations: READS,
1920
+ description: "List every owned primitive available to `graft add` \u2014 shadcn-style copy-in blocks / fields / access rules / bundles (name, type, one-line description, and any registry items it pulls in). Use describe_item for the full details, then install with `graft add <name>` from the CLI. MCP browses what exists; the CLI installs it.",
1921
+ inputSchema: {}
1922
+ },
1923
+ () => guarded(() => ({
1924
+ items: listItems(options.registryRoot).map((item) => ({
1925
+ name: item.name,
1926
+ type: item.type,
1927
+ description: item.description,
1928
+ registryDependencies: item.registryDependencies
1929
+ }))
1930
+ }))
1931
+ );
1932
+ server.registerTool(
1933
+ "describe_item",
1934
+ {
1935
+ title: "Describe a registry item",
1936
+ outputSchema: describeItemOutput,
1937
+ annotations: READS,
1938
+ description: "Full details for one owned primitive: type, description, the files it writes into the project, npm dependencies to install, the registry items it pulls in first, and whether it ships an llms.txt fragment. Use list_registry for names; install with `graft add <name>` (CLI). MCP does not install.",
1939
+ inputSchema: {
1940
+ name: z11.string().describe("Item name as returned by list_registry")
1941
+ }
1942
+ },
1943
+ ({ name }) => guarded(() => describeItem(loadItem(name, options.registryRoot)))
1944
+ );
1945
+ };
1946
+
1947
+ // src/tools/resources.ts
1948
+ import { readFileSync as readFileSync4 } from "fs";
1949
+ import { GraftError as GraftError9 } from "@usegraft/contracts";
1950
+ import { readCollectionDocs as readCollectionDocs3, resolveContained as resolveContained3 } from "@usegraft/compiler";
1951
+ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
1952
+ var isFileAuthoritative = (authority) => authority !== "db-authoritative";
1953
+ var asString = (value) => typeof value === "string" && value !== "" ? value : void 0;
1954
+ var registerDocumentResources = (server, deps) => {
1955
+ const { branchId, collections, contentDir } = deps;
1956
+ const fileCollections = () => Object.values(collections).filter((collection) => isFileAuthoritative(collection.describe().authority)).map((collection) => collection.describe().name);
1957
+ const allDocuments = () => {
1958
+ const out = [];
1959
+ for (const name of fileCollections()) {
1960
+ try {
1961
+ for (const doc of readCollectionDocs3(contentDir, name, collections[name])) {
1962
+ out.push({
1963
+ collection: name,
1964
+ slug: doc.slug,
1965
+ sourcePath: doc.sourcePath,
1966
+ title: asString(doc.data.title),
1967
+ description: asString(doc.data.description)
1968
+ });
1969
+ }
1970
+ } catch {
1971
+ }
1972
+ }
1973
+ return out;
1974
+ };
1975
+ server.registerResource(
1976
+ "document",
1977
+ new ResourceTemplate(documentUriTemplate(branchId), {
1978
+ list: () => ({
1979
+ resources: allDocuments().map((doc) => ({
1980
+ uri: documentUri(branchId, doc.collection, doc.slug),
1981
+ name: `${doc.collection}/${doc.slug}`,
1982
+ title: doc.title ?? doc.slug,
1983
+ description: doc.description ?? `Authored MDX at ${doc.sourcePath}`,
1984
+ mimeType: "text/markdown"
1985
+ }))
1986
+ }),
1987
+ // The URI variables autocomplete from what actually exists, and the slug
1988
+ // list narrows to the collection already chosen — the context carries the
1989
+ // variables filled in so far.
1990
+ complete: {
1991
+ collection: (value) => fileCollections().filter((name) => name.startsWith(value)),
1992
+ slug: (value, context) => {
1993
+ const chosen = context?.arguments?.collection;
1994
+ return allDocuments().filter((doc) => chosen === void 0 || doc.collection === chosen).map((doc) => doc.slug).filter((slug) => slug.startsWith(value));
1995
+ }
1996
+ }
1997
+ }),
1998
+ {
1999
+ title: "Authored document",
2000
+ description: "One MDX document as authored, frontmatter and body, read from the content directory rather than the index \u2014 so it reflects the working tree, not the last compile. Attach it as context; edit it with write_content.",
2001
+ mimeType: "text/markdown"
2002
+ },
2003
+ (uri, variables) => guardedResource(() => {
2004
+ const collectionName = String(variables.collection);
2005
+ const slug = String(variables.slug);
2006
+ const collection = requireCollection(collections, collectionName);
2007
+ if (!isFileAuthoritative(collection.describe().authority)) {
2008
+ throw new GraftError9({
2009
+ code: "AUTHORITY_MISMATCH",
2010
+ message: `Collection "${collectionName}" is db-authoritative \u2014 its records live in Postgres, not as MDX files, so they have no document resource.`,
2011
+ fix: "Read these through the collection's typed functions (see list_functions), not as a resource.",
2012
+ details: { collection: collectionName, authority: collection.describe().authority }
2013
+ });
2014
+ }
2015
+ const docs = readCollectionDocs3(contentDir, collectionName, collection);
2016
+ const doc = docs.find((candidate) => candidate.slug === slug);
2017
+ if (!doc) {
2018
+ throw new GraftError9({
2019
+ code: "DOCUMENT_NOT_FOUND",
2020
+ message: `No document "${slug}" in collection "${collectionName}".`,
2021
+ fix: `Known slugs: ${docs.map((d) => d.slug).join(", ") || "(none)"}. List the document resources, or author it with write_content.`,
2022
+ details: { collection: collectionName, slug }
2023
+ });
2024
+ }
2025
+ return {
2026
+ contents: [
2027
+ {
2028
+ uri: uri.href,
2029
+ mimeType: "text/markdown",
2030
+ // Contained rather than joined: the scan that produced
2031
+ // sourcePath happily lists a symlink, and this mount can be the
2032
+ // unauthenticated docs server, where following one would serve
2033
+ // bytes from outside the content tree to anybody who asks.
2034
+ text: readFileSync4(
2035
+ resolveContained3(contentDir, doc.sourcePath, { label: "document source" }),
2036
+ "utf8"
2037
+ )
2038
+ }
2039
+ ]
2040
+ };
2041
+ })
2042
+ );
2043
+ };
2044
+ var registerSchemaResource = (server, deps) => {
2045
+ const { branchId, collections, functionsByName } = deps;
2046
+ server.registerResource(
2047
+ "schema",
2048
+ schemaUri(branchId),
2049
+ {
2050
+ title: "Project schema",
2051
+ description: "Every collection with its typed fields, and every registered function \u2014 the same payload as describe_schema. Attach it once instead of calling the tool each time.",
2052
+ mimeType: "application/json"
2053
+ },
2054
+ (uri) => {
2055
+ const description = {
2056
+ collections: Object.values(collections).map((collection) => {
2057
+ const descriptor = collection.describe();
2058
+ return { ...descriptor, fields: descriptor.fields.map(teachAssetFields) };
2059
+ }),
2060
+ functions: [...functionsByName.values()].map((fn) => fn.describe())
2061
+ };
2062
+ return {
2063
+ contents: [
2064
+ {
2065
+ uri: uri.href,
2066
+ mimeType: "application/json",
2067
+ text: JSON.stringify(description, null, 2)
2068
+ }
2069
+ ]
2070
+ };
2071
+ }
2072
+ );
2073
+ };
2074
+
2075
+ // src/server.ts
2076
+ function buildServer(options, register) {
2077
+ const { contentDir, collections } = options;
2078
+ const branchId = options.branchId ?? "main";
2079
+ const staticIndexPath = options.db === void 0 ? options.staticIndexPath : void 0;
2080
+ const maybeDb = options.db;
2081
+ if (maybeDb === void 0 && staticIndexPath === void 0) {
2082
+ throw new GraftError10({
2083
+ code: "CONFIG_INVALID",
2084
+ message: "createGraftMcp needs an index: pass `db` (Postgres) or `staticIndexPath`.",
2085
+ fix: "Pass `db` from createDb(DATABASE_URL), or `staticIndexPath` pointing at the compiled artifact (.graft/index.db) for a static project."
2086
+ });
2087
+ }
2088
+ const requireDb = (feature, insteadDo) => {
2089
+ if (maybeDb !== void 0) return maybeDb;
2090
+ throw new GraftError10({
2091
+ code: "NEEDS_DATABASE",
2092
+ message: `${feature} needs the Postgres index; this project serves a static index (${staticIndexPath}).`,
2093
+ fix: `${insteadDo} To move this project to the Postgres tier: set DATABASE_URL, change graft.config to \`export const index = "postgres"\`, run \`graft db migrate\`, then \`graft compile\`.`,
2094
+ details: { feature, index: "static" }
2095
+ });
2096
+ };
2097
+ const requireScope = (tool, scope) => {
2098
+ const actor = options.connectionActor;
2099
+ if (actor === void 0) {
2100
+ if (options.actor === void 0) return;
2101
+ throw new GraftError10({
2102
+ code: "CONFIG_INVALID",
2103
+ message: `${tool} cannot be authorized: this server has an actor resolver but was given no connectionActor.`,
2104
+ fix: "Pass `connectionActor` alongside `actor` when building the server \u2014 createGraftMcpHandler does this for you from the bearer it verified. Without it every scope check passes and write tools are ungated.",
2105
+ details: { tool, required: scope }
2106
+ });
2107
+ }
2108
+ if (actor.kind === "anonymous") return;
2109
+ if ((actor.scopes ?? []).includes(scope)) return;
2110
+ throw new GraftError10({
2111
+ code: "UNAUTHORIZED",
2112
+ message: `${tool} requires the "${scope}" scope, and this credential does not carry it.`,
2113
+ fix: `Mint a token whose scope claim includes "${scope}" (for \`graft serve\`, add it to GRAFT_DEV_SCOPES). Content authoring, asset upload and approval decisions are deliberately separate from ordinary read scopes.`,
2114
+ details: { tool, required: scope, held: actor.scopes ?? [] }
2115
+ });
2116
+ };
2117
+ const requireDecider = () => {
2118
+ const actor = options.connectionActor;
2119
+ if (actor === void 0 || actor.kind === "anonymous" || !actor.id) {
2120
+ throw new GraftError10({
2121
+ code: "UNAUTHORIZED",
2122
+ message: "decide_approval needs to know who is deciding, and this connection is not authenticated as anyone.",
2123
+ fix: "Connect with `Authorization: Bearer <token>` from a trusted issuer (or set GRAFT_DEV_TOKEN for `graft mcp`). Deciding anonymously would make the requester-cannot-decide check meaningless, so it is refused rather than attributed to a placeholder.",
2124
+ details: { tool: "decide_approval", actor: actor?.kind ?? "anonymous" }
2125
+ });
2126
+ }
2127
+ return { kind: actor.kind, id: actor.id };
2128
+ };
2129
+ const projectContent = async () => staticIndexPath === void 0 ? compile({
2130
+ contentDir,
2131
+ collections,
2132
+ db: requireDb("compile", ""),
2133
+ branchId,
2134
+ mdxTrust: options.mdxTrust
2135
+ }) : compileStatic({
2136
+ contentDir,
2137
+ collections,
2138
+ indexPath: staticIndexPath,
2139
+ mdxTrust: options.mdxTrust
2140
+ });
2141
+ const searchIndex = async (query) => {
2142
+ if (staticIndexPath === void 0) {
2143
+ return searchContent(requireDb("search_content", ""), query);
2144
+ }
2145
+ const index = await openStaticIndex(staticIndexPath);
2146
+ try {
2147
+ return await index.searchContent({
2148
+ query: query.query,
2149
+ collections: query.collections,
2150
+ limit: query.limit
2151
+ });
2152
+ } finally {
2153
+ await index.close();
2154
+ }
2155
+ };
2156
+ const functions = options.functions ?? {};
2157
+ const functionsByName = /* @__PURE__ */ new Map();
2158
+ for (const fn of Object.values(functions)) functionsByName.set(fn.name, fn);
2159
+ let scopePromise;
2160
+ const getScope = () => {
2161
+ scopePromise ??= options.scope ? Promise.resolve(options.scope) : resolveBranchScope(requireDb("Branch scope resolution", ""), branchId);
2162
+ return scopePromise;
2163
+ };
2164
+ let functionsHandler;
2165
+ const getFunctionsHandler = () => {
2166
+ functionsHandler ??= createFunctionsHandler({
2167
+ functions,
2168
+ db: requireDb(
2169
+ "run_function",
2170
+ "Typed functions read and write operational data in Postgres, so a static project has none."
2171
+ ),
2172
+ branch: branchId,
2173
+ actor: options.actor,
2174
+ approvalPolicy: options.approvalPolicy,
2175
+ rateLimit: options.rateLimit,
2176
+ gitSha: options.gitSha,
2177
+ audit: options.audit,
2178
+ approvals: options.approvals
2179
+ });
2180
+ return functionsHandler;
2181
+ };
2182
+ const deleteContentFn = defineFunction({
2183
+ name: "delete_content",
2184
+ kind: "mutation",
2185
+ destructive: true,
2186
+ public: true,
2187
+ description: "Delete an authored MDX document and recompile (MCP delete_content tool).",
2188
+ returns: "{ deleted, branch, gitSha, changes }",
2189
+ input: {
2190
+ collection: field.string({ description: "Collection name" }),
2191
+ slug: field.string({ description: "Document slug to delete" })
2192
+ },
2193
+ handler: async ({ input }) => {
2194
+ const collection = requireCollection(collections, input.collection);
2195
+ const doc = findDoc(contentDir, input.collection, collection, input.slug);
2196
+ unlinkSync(resolveContained4(contentDir, doc.sourcePath, { label: "document path" }));
2197
+ const result = await projectContent();
2198
+ return {
2199
+ deleted: doc.sourcePath,
2200
+ branch: branchId,
2201
+ gitSha: result.gitSha,
2202
+ changes: result.changes
2203
+ };
2204
+ }
2205
+ });
2206
+ let deleteHandler;
2207
+ const getDeleteHandler = () => {
2208
+ deleteHandler ??= createFunctionsHandler({
2209
+ // The one-shot, input-bound human approval lives in Postgres. Rather than
2210
+ // silently downgrading to an ungated delete, a static project is told to
2211
+ // do it the way git already makes safe: delete the file and recompile.
2212
+ db: requireDb(
2213
+ "delete_content",
2214
+ "Its human approval gate is a Postgres table, and dropping the gate would make the delete ungated. In a static project, delete the file and recompile \u2014 git is authoritative, so the file IS the document and git history is the undo."
2215
+ ),
2216
+ functions: { delete_content: deleteContentFn },
2217
+ branch: branchId,
2218
+ actor: options.actor,
2219
+ rateLimit: options.rateLimit,
2220
+ gitSha: options.gitSha,
2221
+ audit: options.audit,
2222
+ approvals: options.approvals
2223
+ });
2224
+ return deleteHandler;
2225
+ };
2226
+ let storagePromise;
2227
+ const getStorage = () => {
2228
+ storagePromise ??= (async () => {
2229
+ if (options.storage) {
2230
+ return typeof options.storage === "function" ? options.storage() : options.storage;
2231
+ }
2232
+ try {
2233
+ return createStorage(storageConfigFromEnv());
2234
+ } catch (error) {
2235
+ throw new GraftError10({
2236
+ code: "ENV_VAR_MISSING",
2237
+ message: error instanceof Error ? error.message : String(error),
2238
+ fix: "Set S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY, and S3_BUCKET in the MCP server's environment (.env), then retry.",
2239
+ details: { variables: ["S3_ENDPOINT", "S3_ACCESS_KEY", "S3_SECRET_KEY", "S3_BUCKET"] }
2240
+ });
2241
+ }
2242
+ })();
2243
+ return storagePromise;
2244
+ };
2245
+ const server = new McpServer({
2246
+ name: options.name ?? "graft",
2247
+ version: options.version ?? "0.0.0"
2248
+ });
2249
+ const deps = {
2250
+ options,
2251
+ contentDir,
2252
+ collections,
2253
+ branchId,
2254
+ staticIndexPath,
2255
+ requireDb,
2256
+ requireScope,
2257
+ requireDecider,
2258
+ projectContent,
2259
+ searchIndex,
2260
+ getScope,
2261
+ functions,
2262
+ functionsByName,
2263
+ getFunctionsHandler,
2264
+ getDeleteHandler,
2265
+ getStorage,
2266
+ // Absent unless the mount opted in. The default stays the out-of-band
2267
+ // flow, which is the one a remote agent with no human attached must get.
2268
+ elicitApproval: options.approvalElicitation ? createApprovalElicitor({
2269
+ server,
2270
+ db: () => requireDb(
2271
+ "approval elicitation",
2272
+ "Approvals gate destructive operations on operational data, which a static project does not have."
2273
+ ),
2274
+ decider: options.approvalElicitation.decider
2275
+ }) : void 0
2276
+ };
2277
+ register(server, deps);
2278
+ return server;
2279
+ }
2280
+ var FULL_SURFACE = (server, deps) => {
2281
+ registerCollectionIntrospection(server, deps);
2282
+ registerFunctionIntrospection(server, deps);
2283
+ registerFunctionTools(server, deps);
2284
+ registerRegistryTools(server, deps);
2285
+ registerContentReadTools(server, deps);
2286
+ registerContentWriteTools(server, deps);
2287
+ registerAssetTools(server, deps);
2288
+ registerBranchTools(server, deps);
2289
+ registerApprovalTools(server, deps);
2290
+ registerErrorTools(server, deps);
2291
+ registerPackageTools(server, deps);
2292
+ registerDocumentResources(server, deps);
2293
+ registerSchemaResource(server, deps);
2294
+ registerContentPrompts(server, deps);
2295
+ };
2296
+ var DOCS_SURFACE = (server, deps) => {
2297
+ registerCollectionIntrospection(server, deps);
2298
+ registerContentReadTools(server, deps);
2299
+ registerErrorTools(server, deps);
2300
+ registerPackageTools(server, deps);
2301
+ registerDocumentResources(server, deps);
2302
+ };
2303
+ function createGraftMcp(options) {
2304
+ return buildServer(options, FULL_SURFACE);
2305
+ }
2306
+ function createDocsMcp(options) {
2307
+ return buildServer({ ...options, functions: void 0 }, DOCS_SURFACE);
2308
+ }
2309
+
2310
+ // src/http.ts
2311
+ import { GraftError as GraftError11 } from "@usegraft/contracts";
2312
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
2313
+ function jsonRpcError(status, code, message, headers) {
2314
+ return Response.json({ jsonrpc: "2.0", error: { code, message }, id: null }, { status, headers });
2315
+ }
2316
+ function createGraftMcpHandler(options) {
2317
+ const { actor: resolveActor, allowAnonymous, ...serverOptions } = options;
2318
+ if (resolveActor === void 0 && allowAnonymous !== true) {
2319
+ throw new GraftError11({
2320
+ code: "CONFIG_INVALID",
2321
+ message: "createGraftMcpHandler was given no way to authenticate callers, and this endpoint serves content writes, asset uploads and approval decisions.",
2322
+ fix: "Pass `actor` \u2014 the @usegraft/auth `createActorResolver` seam, the same one the functions route uses. For a local dev server with no auth at all, pass `allowAnonymous: true` explicitly; never do that on anything reachable from a network."
2323
+ });
2324
+ }
2325
+ if (serverOptions.approvalElicitation !== void 0) {
2326
+ throw new GraftError11({
2327
+ code: "CONFIG_INVALID",
2328
+ message: "createGraftMcpHandler cannot use `approvalElicitation`: over HTTP the client being asked to approve is the agent that made the call.",
2329
+ fix: "Drop `approvalElicitation` from this handler. Approvals over HTTP go through the out-of-band path \u2014 the call returns DESTRUCTIVE_OP_REQUIRES_APPROVAL with an id, a human runs `graft approve <id>`, and the caller retries with it. Elicitation is for a stdio server (`createGraftMcp` / `graft mcp --elicit-approvals`) whose operator is at the machine."
2330
+ });
2331
+ }
2332
+ return async (request) => {
2333
+ if (request.method !== "POST") {
2334
+ return jsonRpcError(405, -32e3, "Method not allowed: this server is stateless (POST only)", {
2335
+ allow: "POST"
2336
+ });
2337
+ }
2338
+ let actor;
2339
+ if (resolveActor) {
2340
+ try {
2341
+ actor = await resolveActor(request);
2342
+ } catch (err) {
2343
+ const message = err instanceof GraftError11 ? `${err.message} ${err.fix ?? ""}`.trim() : "Unauthorized";
2344
+ return jsonRpcError(401, -32001, message);
2345
+ }
2346
+ if (allowAnonymous !== true && actor.kind === "anonymous") {
2347
+ return jsonRpcError(
2348
+ 401,
2349
+ -32001,
2350
+ "Unauthorized: this MCP endpoint requires authentication. Send `Authorization: Bearer <token>` from a trusted issuer."
2351
+ );
2352
+ }
2353
+ }
2354
+ const server = createGraftMcp({
2355
+ ...serverOptions,
2356
+ actor: resolveActor,
2357
+ // Tools that need to know WHO is calling (rather than forward a
2358
+ // credential) read this. It is the same identity the check above just
2359
+ // verified, so a tool can never be told a different one.
2360
+ ...actor === void 0 ? {} : { connectionActor: actor },
2361
+ defaultAuthorization: request.headers.get("authorization") ?? serverOptions.defaultAuthorization
2362
+ });
2363
+ const transport = new WebStandardStreamableHTTPServerTransport({
2364
+ sessionIdGenerator: void 0,
2365
+ enableJsonResponse: true
2366
+ });
2367
+ try {
2368
+ await server.connect(transport);
2369
+ return await transport.handleRequest(request);
2370
+ } finally {
2371
+ void server.close().catch(() => void 0);
2372
+ }
2373
+ };
2374
+ }
2375
+ function createDocsMcpHandler(options) {
2376
+ return async (request) => {
2377
+ if (request.method !== "POST") {
2378
+ return jsonRpcError(405, -32e3, "Method not allowed: this server is stateless (POST only)", {
2379
+ allow: "POST"
2380
+ });
2381
+ }
2382
+ const server = createDocsMcp(options);
1326
2383
  const transport = new WebStandardStreamableHTTPServerTransport({
1327
2384
  sessionIdGenerator: void 0,
1328
2385
  enableJsonResponse: true
@@ -1350,6 +2407,8 @@ async function serveStdio(server) {
1350
2407
  }
1351
2408
  export {
1352
2409
  ERROR_KNOWLEDGE,
2410
+ createDocsMcp,
2411
+ createDocsMcpHandler,
1353
2412
  createGraftMcp,
1354
2413
  createGraftMcpHandler,
1355
2414
  explainCode,