@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/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # @usegraft/mcp
2
+
3
+ > An MCP server over your content, schema, and typed functions, with errors an agent can act on.
4
+
5
+ Part of [Graft](https://github.com/AndersonDesign1/graft), a CMS built so an AI agent is the primary operator.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm i @usegraft/mcp
11
+ ```
12
+
13
+ ## Serve on stdio
14
+
15
+ For `.mcp.json` and local agents. `graft mcp` does this for you.
16
+
17
+ ```ts
18
+ import { createGraftMcp, serveStdio } from "@usegraft/mcp";
19
+
20
+ const server = createGraftMcp({ contentDir, collections, functions, db });
21
+ await serveStdio(server);
22
+ ```
23
+
24
+ ## Serve over HTTP
25
+
26
+ ```ts
27
+ import { createGraftMcpHandler } from "@usegraft/mcp";
28
+
29
+ export const POST = createGraftMcpHandler({
30
+ contentDir,
31
+ collections,
32
+ db,
33
+ actor: resolveActor,
34
+ connectionActor: resolveConnectionActor,
35
+ });
36
+ ```
37
+
38
+ `connectionActor` is not optional in practice. Without it every write-tool scope check is silently disabled, so pass it whenever a tool can write.
39
+
40
+ ## Tools
41
+
42
+ Content: `list_content`, `read_content`, `write_content`, `delete_content`, `search_content`. Introspection: `describe_schema`, `list_functions`, `describe_function`. Execution: `run_function`. Registry: `list_registry`, `describe_item`. Approvals: `list_approvals`, `decide_approval`.
43
+
44
+ ## Defaults that fail closed
45
+
46
+ - Anonymous callers are refused unless explicitly allowed.
47
+ - `write_content` requires the `content:write` scope, and being authenticated earns nothing on its own.
48
+ - `delete_content` is destructive and always human-gated: the first call files an approval and fails with its id.
49
+ - Authored MDX is refused if it contains `{…}` expressions, `import`, `export` or spread attributes, because rendering evaluates MDX as JavaScript on the server.
50
+
51
+ ---
52
+
53
+ MIT. [Repository](https://github.com/AndersonDesign1/graft) · [Changelog](https://github.com/AndersonDesign1/graft/blob/main/packages/mcp/CHANGELOG.md) · [Security policy](https://github.com/AndersonDesign1/graft/blob/main/SECURITY.md)
package/dist/index.d.ts CHANGED
@@ -2,8 +2,16 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { Storage } from '@usegraft/assets';
3
3
  import { AnyCollection, AnyGraftFunction, FunctionActor, RateLimit } from '@usegraft/core';
4
4
  import { Database, BranchScope, AuditStore, ApprovalStore } from '@usegraft/db';
5
+ import { MdxTrust } from '@usegraft/mdx-safety';
5
6
  import { ErrorCode } from '@usegraft/contracts';
6
7
 
8
+ /**
9
+ * How a Graft MCP server is configured.
10
+ *
11
+ * Split out of server.ts so tool modules can name it without importing the
12
+ * server that hosts them.
13
+ */
14
+
7
15
  interface GraftMcpOptions {
8
16
  /** Absolute path to the content root (documents live at <contentDir>/<collection>/<slug>.mdx). */
9
17
  contentDir: string;
@@ -27,6 +35,13 @@ interface GraftMcpOptions {
27
35
  * projects still work.
28
36
  */
29
37
  functions?: Record<string, AnyGraftFunction>;
38
+ /**
39
+ * How much of MDX authored bodies may be, from `mdxTrust` in graft.config.ts.
40
+ * Defaults to "restricted". Applies to the whole tree on every projection,
41
+ * not just to bodies arriving through write_content, because compile re-reads
42
+ * every authored file including the ones that came from git.
43
+ */
44
+ mdxTrust?: MdxTrust;
30
45
  /** Content branch to project into. Defaults to "main". */
31
46
  branchId?: string;
32
47
  /**
@@ -46,7 +61,45 @@ interface GraftMcpOptions {
46
61
  * createFunctionsHandler / createGraftMcpHandler. Defaults to anonymous.
47
62
  */
48
63
  actor?: (request: Request) => FunctionActor | Promise<FunctionActor>;
49
- /** Forwarded to createFunctionsHandler for run_function. */
64
+ /**
65
+ * The identity this connection authenticated as, already resolved.
66
+ *
67
+ * `actor` above resolves a *Request*, which tools do not have — they are
68
+ * JSON-RPC calls on an established connection. Tools that need to know who
69
+ * is calling (rather than merely forwarding a credential) read this instead.
70
+ * The HTTP handler sets it from the bearer it already verified; `graft mcp`
71
+ * sets it from the dev-token identity. Absent means anonymous.
72
+ */
73
+ connectionActor?: FunctionActor;
74
+ /**
75
+ * Directory `put_asset`'s `path` argument may read from, enabling that
76
+ * argument at all.
77
+ *
78
+ * Unset — which is every remote mount — means `put_asset` has no `path`
79
+ * argument: a remote agent sends bytes as base64 or nothing. It used to pass
80
+ * the raw string to readFileSync with no containment whatsoever, upload the
81
+ * result under a key of the caller's choosing, and return a fetchable URL, so
82
+ * `{ path: "/srv/app/.env" }` was a one-call read of DATABASE_URL, dev tokens
83
+ * and S3 credentials on any HTTP-mounted server.
84
+ *
85
+ * `graft mcp` sets it to the project directory: a local agent uploading a
86
+ * hero image from the repo is the case the argument exists for.
87
+ */
88
+ localUploadRoot?: string;
89
+ /**
90
+ * Forwarded to createFunctionsHandler for run_function.
91
+ *
92
+ * Deliberately narrower than the core option, which also accepts
93
+ * `"unattended"`. That policy exists for a deployment with no human to ask —
94
+ * a scheduled job, a CI step — and an MCP mount is the opposite of that: it
95
+ * exists because an agent is calling it, and the agent is the party the gate
96
+ * is there to stop. Allowing it here would let one option turn every
97
+ * destructive tool an agent can reach into an ungated one.
98
+ *
99
+ * A headless deployment that genuinely wants it still has it, on
100
+ * `createFunctionsHandler` and through `approvalPolicy` in graft.config.ts on
101
+ * `graft serve`'s /api/fn routes. It just does not reach the tool surface.
102
+ */
50
103
  approvalPolicy?: "none" | "human";
51
104
  rateLimit?: RateLimit;
52
105
  gitSha?: string;
@@ -58,6 +111,36 @@ interface GraftMcpOptions {
58
111
  * GRAFT_DEV_TOKEN; the HTTP handler forwards the caller's own header.
59
112
  */
60
113
  defaultAuthorization?: string;
114
+ /**
115
+ * Ask the connected client's human to decide a pending approval in-band,
116
+ * instead of failing with an id and waiting for someone to run
117
+ * `graft approve`.
118
+ *
119
+ * **Off unless configured, and it belongs only on a mount whose human is
120
+ * actually present** — a local stdio server, a desktop client. A remote or
121
+ * public mount must never set it: there is nobody at the other end to ask,
122
+ * and an elicitation nobody answers is a destructive call left hanging.
123
+ *
124
+ * The reason this needs an explicit `decider` rather than reusing the
125
+ * connection's identity is the invariant underneath: `decideApproval`
126
+ * enforces `requested_by_id <> decided_by` in the UPDATE's own WHERE, so a
127
+ * requester deciding its own approval is refused by Postgres, not by a guard
128
+ * that could be forgotten. Elicitation changes *how the human is asked*, not
129
+ * *who is recorded as having answered*. Name the operator sitting at the
130
+ * machine; if that operator is also the requester, the decision is refused
131
+ * exactly as it would be from the CLI.
132
+ *
133
+ * Deciding is a plain UPDATE on `approvals`, which the hardened runtime role
134
+ * deliberately cannot perform (see `graft harden`). On such a deployment this
135
+ * fails, correctly and by construction.
136
+ */
137
+ approvalElicitation?: {
138
+ /** The operator a decision is attributed to. Never the requester. */
139
+ decider: {
140
+ kind: string;
141
+ id: string;
142
+ };
143
+ };
61
144
  /**
62
145
  * Audit / approval stores for run_function. Defaults match createFunctionsHandler
63
146
  * (db-backed). Pass `audit: false` in unit tests that do not hit a real DB.
@@ -76,7 +159,26 @@ interface GraftMcpOptions {
76
159
  */
77
160
  storage?: Storage | (() => Storage | Promise<Storage>);
78
161
  }
162
+
163
+ /**
164
+ * What a public documentation server needs, which is much less than the full
165
+ * one. No `functions`, no `actor`, no asset store, no approval elicitation:
166
+ * omitting them from the type is what keeps them from being configured by
167
+ * accident on a mount that answers the internet.
168
+ */
169
+ type DocsMcpOptions = Pick<GraftMcpOptions, "contentDir" | "collections" | "db" | "staticIndexPath" | "branchId" | "scope" | "name" | "version">;
79
170
  declare function createGraftMcp(options: GraftMcpOptions): McpServer;
171
+ /**
172
+ * A public, read-only documentation server.
173
+ *
174
+ * A separate factory rather than a flag on `createGraftMcp`, because the
175
+ * safety property worth having is that the closed mount gains no new way to be
176
+ * opened. There is no option here that widens the surface; reaching the wider
177
+ * one means calling the other function.
178
+ *
179
+ * Serve it at `/mcp` on the docs domain, which is where clients look.
180
+ */
181
+ declare function createDocsMcp(options: DocsMcpOptions): McpServer;
80
182
 
81
183
  interface GraftMcpHandlerOptions extends GraftMcpOptions {
82
184
  /**
@@ -86,13 +188,36 @@ interface GraftMcpHandlerOptions extends GraftMcpOptions {
86
188
  */
87
189
  actor?: (request: Request) => FunctionActor | Promise<FunctionActor>;
88
190
  /**
89
- * Reject anonymous callers with 401. Off by default (a dev server on
90
- * localhost); turn it on for anything reachable from outside.
191
+ * Serve callers who did not authenticate.
192
+ *
193
+ * Off by default, and deliberately phrased as an opt-*in* to insecurity: this
194
+ * handler is built to be embedded in a Next.js route, a self-host container,
195
+ * Vercel Fluid, or a Worker, and the previous `requireActor` flag defaulted to
196
+ * off — so forgetting it silently published write_content, put_asset,
197
+ * delete_content and decide_approval to anyone who found the URL.
198
+ *
199
+ * Constructing a handler with neither `actor` nor `allowAnonymous: true`
200
+ * throws, so a deployer who forgets gets a startup failure with a fix line
201
+ * rather than an open endpoint.
91
202
  */
92
- requireActor?: boolean;
203
+ allowAnonymous?: boolean;
93
204
  }
94
205
  type GraftMcpHandler = (request: Request) => Promise<Response>;
95
206
  declare function createGraftMcpHandler(options: GraftMcpHandlerOptions): GraftMcpHandler;
207
+ /**
208
+ * A public documentation MCP endpoint, over the same stateless transport.
209
+ *
210
+ * Deliberately has no `actor` and no `allowAnonymous`. The full handler refuses
211
+ * to start without one of them because it serves writes, uploads and approval
212
+ * decisions; this one serves documentation, so there is nothing to authenticate
213
+ * and nothing to accidentally leave open. That is the point of it being a
214
+ * separate function: the closed endpoint gains no new way to be opened.
215
+ *
216
+ * Mount it at `/mcp` on the docs domain, which is where clients look —
217
+ * Mintlify generates one there for every site it hosts, and Cloudflare runs a
218
+ * documentation server separately from its authenticated API server.
219
+ */
220
+ declare function createDocsMcpHandler(options: DocsMcpOptions): GraftMcpHandler;
96
221
 
97
222
  /**
98
223
  * The self-teaching error knowledge base behind the `explain_error` tool.
@@ -120,6 +245,12 @@ declare function explainCode(code: string): ErrorExplanation | undefined;
120
245
  * `createGraftMcp` builds the server; `serveStdio`
121
246
  * binds it to stdio for `.mcp.json` / `graft mcp`; `createGraftMcpHandler`
122
247
  * serves it over Streamable HTTP as a stateless `Request → Response` handler.
248
+ *
249
+ * `createDocsMcp` / `createDocsMcpHandler` build the other one: a public,
250
+ * read-only documentation server for `/mcp` on a docs domain. It is a separate
251
+ * factory rather than a flag, so the authenticated endpoint gains no new way to
252
+ * be opened.
253
+ *
123
254
  * See docs/design-notes/agent-mcp.md.
124
255
  */
125
256
 
@@ -134,4 +265,4 @@ declare function explainCode(code: string): ErrorExplanation | undefined;
134
265
  */
135
266
  declare function serveStdio(server: McpServer): Promise<void>;
136
267
 
137
- export { ERROR_KNOWLEDGE, type ErrorExplanation, type GraftMcpHandler, type GraftMcpHandlerOptions, type GraftMcpOptions, createGraftMcp, createGraftMcpHandler, explainCode, serveStdio };
268
+ export { type DocsMcpOptions, ERROR_KNOWLEDGE, type ErrorExplanation, type GraftMcpHandler, type GraftMcpHandlerOptions, type GraftMcpOptions, createDocsMcp, createDocsMcpHandler, createGraftMcp, createGraftMcpHandler, explainCode, serveStdio };