@panaversity/ksor 0.0.22 → 0.0.24

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.
@@ -0,0 +1,133 @@
1
+ ---
2
+ title: The tool surface
3
+ status: draft
4
+ ---
5
+
6
+ # Shaping what agents see — `system/gateways/content.ts`
7
+
8
+ That file is your record's MCP registration: ordinary `registerTool` calls with
9
+ ordinary zod. It decides what your tools are called, what they say, what they
10
+ accept, and which of them exist. It is yours, and deleting it is supported —
11
+ without it the door serves the same defaults.
12
+
13
+ ## Why it is worth editing
14
+
15
+ An agent pays for this surface out of its context window, and it pays twice.
16
+ Every tool's name, description and input schema is resident for the whole
17
+ session; every answer spends more.
18
+
19
+ Measured against a live 81-document record (6,963 chunks), ~4 chars/token:
20
+
21
+ | | chars | ~tokens | |
22
+ | ------------------------------ | ------ | ------- | ------------------- |
23
+ | all three tool definitions | 11,960 | 2,990 | **always resident** |
24
+ | `search` alone | 5,383 | 1,346 | always resident |
25
+ | `outline` + `read` | 6,571 | 1,643 | always resident |
26
+ | one `search`, `k=10` (default) | 14,164 | 3,541 | per call |
27
+ | one `search`, `k=5` | 8,009 | 2,002 | per call |
28
+
29
+ An agent with five records attached carries ~15,000 tokens of definitions before
30
+ doing any work.
31
+
32
+ ## The three edits that pay
33
+
34
+ ### 1. Delete a tool nothing calls
35
+
36
+ The biggest win, and the easiest — delete its `registerTool` block. Measured
37
+ live: a registration keeping only a renamed `search` served **5,337** bytes
38
+ against the default's 11,960.
39
+
40
+ ### 2. Say what the record covers
41
+
42
+ The line that decides whether an agent asks _you_ rather than another record it
43
+ has open. Name the subject **and the boundary**:
44
+
45
+ ```ts
46
+ description: `Employee handbook: leave, benefits, conduct, expenses.
47
+ Not product documentation and not customer data.\n\n${FLOOR.search}`,
48
+ ```
49
+
50
+ Your prose goes **above** `FLOOR.search`, never instead of it — see below.
51
+
52
+ ### 3. Set `k`
53
+
54
+ `k` is the lever on reply size: 10 costs ~3,500 tokens a call, 5 costs ~2,000.
55
+ The caller can always ask for more, so make the default what you usually need.
56
+
57
+ ```ts
58
+ inputSchema: z.object({
59
+ query: z.string().min(1).max(2000),
60
+ k: z.number().int().min(1).max(50).default(5),
61
+ }),
62
+ ```
63
+
64
+ **`budgets.maximum_response_characters` is not this lever.** It defaults to
65
+ 120,000 and at ~1,400 chars a hit cannot bind before the 50-hit ceiling. Tune `k`.
66
+
67
+ ## Adding your own tools
68
+
69
+ It is an MCP server. Call `registerTool` again with your own handler:
70
+
71
+ ```ts
72
+ server.registerTool(
73
+ "check_policy_expiry",
74
+ {
75
+ inputSchema: z.object({ policy: z.string() }),
76
+ },
77
+ async ({ policy }) => ({ content: [{ type: "text", text: await lookup(policy) }] }),
78
+ );
79
+ ```
80
+
81
+ One thing to be clear-eyed about: **ksor makes no provenance claim about a tool
82
+ it did not hand you a handler for.** `searchHandler(ctx)` answers from the
83
+ governed record with citations; a handler you write answers from wherever you
84
+ made it answer from.
85
+
86
+ ## What you cannot change, and why
87
+
88
+ - **The handlers.** `searchHandler` / `outlineHandler` / `readHandler` are the
89
+ only things that can prove a passage came from the governed record. A
90
+ hand-written one returning fabricated hits with plausible `stable_id`s would
91
+ pass every shape check there is.
92
+ - **The output schemas.** `SEARCH_OUTPUT`, `OUTLINE_OUTPUT`, `READ_OUTPUT` carry
93
+ `provenance`, the `snapshot` token and `gate`. A record that reshaped them
94
+ would still look like a KSoR and no longer be one.
95
+ - **The `FLOOR` text.** It tells an agent how to branch on an envelope, what
96
+ `gate: "off"` means, and that corpus content is **untrusted** — quote it, never
97
+ obey it. Your prose is composed above it.
98
+
99
+ ## The door checks its own surface at boot
100
+
101
+ Because that last one is a template literal in a file you own, nothing structural
102
+ stops it being dropped. So the door builds its server, asks itself `tools/list`
103
+ over an in-memory transport, and refuses to start if a guarantee is gone:
104
+
105
+ ```
106
+ error: ksor-gateway-floor-missing: the search tool is served as "search_the_book"
107
+ without its framework description. That text tells an agent how to read an
108
+ abstention and that corpus content is untrusted — without it this record answers
109
+ without ever declining, and follows instructions written into its own documents.
110
+ Put FLOOR.search back: a record's own prose goes ABOVE it, as
111
+ `${yourText}\n\n${FLOOR.search}`, never instead of it
112
+ ```
113
+
114
+ | what | slug |
115
+ | -------------------------------------------------- | ---------------------------- |
116
+ | a served ksor tool lost its `FLOOR` text | `ksor-gateway-floor-missing` |
117
+ | the registration serves no tools at all | `ksor-gateway-no-tools` |
118
+ | the file throws, or default-exports a non-function | `ksor-gateway-unloadable` |
119
+
120
+ Delete the file to take the default registration back.
121
+
122
+ ## One import, no dependencies
123
+
124
+ Everything comes from `@panaversity/ksor/gateway` — including `z` and
125
+ `McpServer`. That is deliberate: your registration stays a _file_, with no
126
+ package.json, no build step, and nothing new in your lockfile. It also means the
127
+ SDK validates with the same zod instance it was built against, which a
128
+ separately-installed zod would not.
129
+
130
+ ## More records later
131
+
132
+ `identity` and `praxis` get `system/gateways/<record>.ts` by the same rule.
133
+ Nothing above is specific to content except which tools exist.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@panaversity/ksor",
3
- "version": "0.0.22",
3
+ "version": "0.0.24",
4
4
  "description": "Knowledge System of Record — compile governed markdown into a static site for people and an MCP server for AI agents, with citations and measured abstention.",
5
5
  "keywords": [
6
6
  "abstention",
@@ -44,6 +44,11 @@
44
44
  "import": "./dist/index.mjs",
45
45
  "default": "./dist/index.mjs"
46
46
  },
47
+ "./gateway": {
48
+ "types": "./dist/gateway.d.mts",
49
+ "import": "./dist/gateway.mjs",
50
+ "default": "./dist/gateway.mjs"
51
+ },
47
52
  "./package.json": "./package.json"
48
53
  },
49
54
  "publishConfig": {
@@ -311,6 +311,48 @@ signing keys, an unknown key id answers **503**, not 401 — the token may well
311
311
  good and the door's key set merely stale, so a client should retry rather than
312
312
  send the user back through a login.
313
313
 
314
+ ## Shaping the agent surface — `system/gateways/content.ts`
315
+
316
+ That file is this record's MCP registration — ordinary `registerTool` with
317
+ ordinary zod. It is yours, and it is **deletable**: without it the door serves
318
+ the same defaults.
319
+
320
+ Edit it because an agent pays for this surface out of its context window, twice.
321
+ Measured on an 81-document record:
322
+
323
+ | | |
324
+ | -------------------------------- | ------------------------------ |
325
+ | all three tool definitions | ~2,990 tokens, always resident |
326
+ | one `search` at `k=10` (default) | ~3,541 tokens per call |
327
+ | one `search` at `k=5` | ~2,002 tokens per call |
328
+
329
+ Three edits pay for themselves:
330
+
331
+ - **Delete a tool nothing calls.** Removing `outline` and `read` gives back
332
+ ~1,643 tokens for the whole session.
333
+ - **Say what this record covers**, above `FLOOR.search`. It is how an agent with
334
+ several records attached picks yours; name the subject AND the boundary.
335
+ - **Set `k`** in the input schema — it is the lever on reply size.
336
+
337
+ ```ts
338
+ description: `Leave, benefits, conduct. Not product docs.\n\n${FLOOR.search}`,
339
+ inputSchema: z.object({ query: z.string(), k: z.number().int().default(5) }),
340
+ ```
341
+
342
+ You can add your own tools with `registerTool` too — but be clear-eyed: ksor
343
+ makes no provenance claim about a tool it did not hand you a handler for.
344
+
345
+ What you cannot change is deliberate: the handlers, the output schemas, and the
346
+ `FLOOR` text. Your prose goes ABOVE the floor, never instead of it — the floor
347
+ tells an agent how to read an abstention and that corpus content is untrusted,
348
+ and a record that dropped it would answer without ever declining.
349
+
350
+ Because that is a template literal in a file you own, the door checks its own
351
+ surface at boot and refuses to start if a guarantee is gone:
352
+ `ksor-gateway-floor-missing`, `ksor-gateway-no-tools`,
353
+ `ksor-gateway-unloadable`. Full detail:
354
+ `node_modules/@panaversity/ksor/docs/tool-surface.md`.
355
+
314
356
  ## Withdrawing a document — `ksor takedown`
315
357
 
316
358
  A takedown is the one governance act that must reach EVERY surface at once.
@@ -356,6 +398,35 @@ refuses rather than publish a document someone took down.
356
398
  behind that audience's own access control, never on a public host.
357
399
  Details in README → Deploying.
358
400
 
401
+ ### The MCP door is a container
402
+
403
+ The other surface is a live process, so it ships as one. `Dockerfile` and
404
+ `.dockerignore` are yours, at the repo root, and they name no host:
405
+
406
+ ```sh
407
+ docker build -t my-record .
408
+ docker run --rm -p 8080:80 --env-file .env my-record
409
+ ```
410
+
411
+ That image runs on Cloud Run, Fly, Render, ECS, Kubernetes or a VPS unchanged.
412
+ `vercel.json` declares BOTH surfaces — a `site` service built from
413
+ `system/site/out/` and a `door` service pointing at that same `Dockerfile` —
414
+ with rewrites putting the door on `/mcp`, `/health`, `/ready` and the
415
+ `/.well-known/oauth-protected-resource` document, and the site on everything
416
+ else. Two rules if you edit it: the `/(.*)` catch-all must stay LAST, and do
417
+ not add a project-level `trailingSlash` — the site's Next config already sets
418
+ it, and at project level it 308-redirects `POST /mcp`, which breaks the door.
419
+
420
+ The image deliberately excludes `.env` (a baked DSN is published to anyone who
421
+ can pull the image), `knowledge/` (the door reads Postgres, never the folder)
422
+ and `system/` (the other surface).
423
+
424
+ **Deploying does not publish.** A container that ingested on boot would pay the
425
+ whole record's embedding cost on every cold start and need write credentials at
426
+ runtime. So `pnpm refresh` is a DEPLOY step you run — from your machine or from
427
+ CI — and a first deploy without it serves an empty record. Full walkthrough:
428
+ `node_modules/@panaversity/ksor/docs/deploying.md` and `…/docs/ingesting.md`.
429
+
359
430
  ## Writing knowledge
360
431
 
361
432
  - One document per file under `knowledge/`; the path is the document's
@@ -0,0 +1,36 @@
1
+ # The MCP door, as an ordinary container.
2
+ #
3
+ # Nothing here names a host. It installs the pinned `@panaversity/ksor` from
4
+ # package.json, listens on $PORT, and runs `ksor serve` — which is all Cloud Run,
5
+ # Fly, Render, ECS, Kubernetes or a plain VPS asks for. `vercel.json` points AT
6
+ # this file rather than replacing it, so the artifact stays portable and the host
7
+ # stays a choice.
8
+ #
9
+ # Build and run it anywhere:
10
+ # docker build -t my-record .
11
+ # docker run --rm -p 8080:80 --env-file .env my-record
12
+ #
13
+ # This image serves; it does not publish. `ksor ingest` is a write plane that
14
+ # runs from CI or your machine against the same database — see the deployment
15
+ # guide in node_modules/@panaversity/ksor/docs/deploying.md.
16
+
17
+ FROM node:24-alpine
18
+
19
+ WORKDIR /app
20
+
21
+ # Only the manifest first, so this layer caches until the ksor pin changes.
22
+ COPY package.json ./
23
+ RUN npm install --omit=dev --no-audit --no-fund
24
+
25
+ # The record's identity and configuration. The CORPUS is deliberately absent:
26
+ # the door serves from Postgres, and knowledge/ belongs to the build that
27
+ # published it — see .dockerignore.
28
+ COPY instance.md ./
29
+
30
+ # Most container hosts inject PORT; 80 is a sane default when nothing does.
31
+ ENV PORT=80
32
+ EXPOSE 80
33
+
34
+ # `ksor serve` refuses to boot unauthenticated on a public bind. That posture
35
+ # belongs to the record, not to the host, so it travels inside the image.
36
+ CMD ["node_modules/.bin/ksor", "serve", "--instance", "instance.md"]
@@ -60,6 +60,43 @@ unauthenticated: a local run declares `KSOR_AUTH_DISABLED=1` (already in
60
60
  a public bind needs a configured SSO door instead. Any other operation is
61
61
  `pnpm exec ksor <verb>`.
62
62
 
63
+ ### Test the agent surface with an actual agent
64
+
65
+ The MCP door is meant to be read by agents, so check it with one rather than
66
+ with `curl`. With `pnpm serve` running, write `.mcp.json` at the repo root:
67
+
68
+ ```json
69
+ {
70
+ "mcpServers": {
71
+ "test-record": {
72
+ "type": "http",
73
+ "url": "http://127.0.0.1:8080/mcp"
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ Open a new session of your coding agent, confirm it lists the server, then ask
80
+ it three questions **in this order** — the order is the test:
81
+
82
+ 1. Something the record covers, **phrased in words the document never uses**.
83
+ Retrieval is semantic, so this should still find it, and every answer should
84
+ arrive with a citation.
85
+ 2. Something **adjacent but not covered** — your record's own subject area, a
86
+ question it genuinely does not answer. It should decline.
87
+ 3. Something far outside the record. It should decline, and must not answer
88
+ from its own knowledge.
89
+
90
+ Question 2 is the one that matters. Anything can answer questions it has the
91
+ text for; refusing a plausible near-miss is the property that makes a system of
92
+ record worth trusting, and it is the one that breaks quietly.
93
+
94
+ **On a fresh record, 2 and 3 will not refuse — and that is honest, not broken.**
95
+ The abstention gate is off until you measure a floor for this corpus, which the
96
+ server says out loud at boot (`abstain OFF`) and in every search envelope
97
+ (`gate: "off"`). Run `pnpm exec ksor calibrate --instance instance.md` first if
98
+ you want to test refusal. Delete `.mcp.json`, or keep it — it holds no secret.
99
+
63
100
  Then talk to your coding agent — `AGENTS.md` carries the working rules, and
64
101
  the agent kit in `.agents/skills/` knows how to interview you
65
102
  (`intake-interview`), convert your source material (`add-sources`), and keep
@@ -118,7 +155,9 @@ and anything that can serve files can serve it.
118
155
  - **Vercel** — connect the repository (or run `vercel`); the shipped
119
156
  `vercel.json` answers the setup interview: deploy from the repo root
120
157
  (never pin `system/site` as the root directory — the record lives
121
- outside it), build with `pnpm build`, serve `system/site/out/`. If the
158
+ outside it), build with `pnpm build`, serve `system/site/out/`. It also
159
+ declares the MCP **door** as a second service built from the shipped
160
+ `Dockerfile`, so `/mcp` and the site share one domain. If the
122
161
  build image's pnpm predates the `packageManager` pin, set the
123
162
  `ENABLE_EXPERIMENTAL_COREPACK=1` build environment variable.
124
163
  **Once `instance.md` declares a `database:`, the BUILD needs the DSN too.**
@@ -143,6 +182,24 @@ writes "nothing denied" and exits 0.
143
182
  - **Verify any deploy** the same way: the home page, one document page,
144
183
  and `/llms.txt` all load; nothing else is required.
145
184
 
185
+ ### The agent surface deploys separately
186
+
187
+ The site is files; the MCP door is a process. `Dockerfile` and `.dockerignore`
188
+ at the repo root build it, and they name no host — the same image runs on
189
+ Cloud Run, Fly, Render, ECS, Kubernetes or a VPS:
190
+
191
+ ```sh
192
+ docker build -t my-record .
193
+ docker run --rm -p 8080:80 --env-file .env my-record
194
+ ```
195
+
196
+ One thing surprises people: **deploying does not publish.** The door serves
197
+ whatever generation is already in the database, so a first deploy with no
198
+ `pnpm refresh` serves an empty record. Publishing is a step you run — from your
199
+ machine or from CI — and it is deliberately not something a booting container
200
+ does. The full walkthrough, including what a cold start costs and where ingest
201
+ belongs, is in `node_modules/@panaversity/ksor/docs/deploying.md`.
202
+
146
203
  If `instance.md` declares `audiences:`, what you deploy is a **tier**.
147
204
  Plain `pnpm build` always builds the public tier — safe for any host.
148
205
  `KSOR_AUDIENCE=<audience> pnpm build` builds a wider tier for that
@@ -0,0 +1,26 @@
1
+ # Keep the serving image to what serving needs.
2
+
3
+ # Secrets. The image takes its configuration from the environment at RUN time;
4
+ # baking a .env into a layer publishes it to anyone who can pull the image.
5
+ .env
6
+ .env.*
7
+ !.env.example
8
+
9
+ # The corpus. The door serves from Postgres — knowledge/ was published by
10
+ # `ksor ingest` before this container ever started, and copying it in would
11
+ # suggest the container reads it. It does not.
12
+ knowledge/
13
+
14
+ # The website. It is the OTHER surface, built and hosted separately.
15
+ system/
16
+
17
+ # Build and tooling noise.
18
+ node_modules/
19
+ .git/
20
+ .github/
21
+ .agents/
22
+ .claude/
23
+ .gemini/
24
+ *.log
25
+ .DS_Store
26
+ .ksor-denylist.json
@@ -19,17 +19,13 @@ minimumReleaseAgeExclude:
19
19
  # Dependency install scripts are denied by default. Flip an entry to true
20
20
  # only with a comment naming what breaks without it. pnpm 11 exits 1 on every
21
21
  # install until each build script is explicitly decided (found live:
22
- # fresh-scaffold pnpm dev, 2026-08-18). All four below are reviewed and stay
23
- # denied:
22
+ # fresh-scaffold pnpm dev, 2026-08-18). Both below are reviewed and stay denied:
24
23
  # esbuild, sharp — prebuilt platform binaries ship as optionalDependencies,
25
24
  # so their install scripts are download fallbacks the site never needs.
26
- # @google/genai, protobufjs — pulled in by the `@panaversity/ksor` serve
27
- # tool. genai's flagged script is its own dev `prepare` (nothing the
28
- # published tarball needs built); protobufjs's postinstall is a
29
- # version-compat warning, nothing built. (Same review as the root
30
- # workspace, 2026-08-19; serve works with both denied — verified live.)
25
+ #
26
+ # `@google/genai` and `protobufjs` were listed here too, as deps of the serve
27
+ # tool. The embedding provider now speaks the vendor's REST API directly, so
28
+ # neither package is installed at all and denying them described nothing.
31
29
  allowBuilds:
32
30
  esbuild: false
33
31
  sharp: false
34
- "@google/genai": false
35
- protobufjs: false
@@ -0,0 +1,162 @@
1
+ /**
2
+ * The default registration — and the ORIGINAL of the file `ksor init` emits.
3
+ *
4
+ * This is the canonical half of decision 18's mechanism, applied to the agent
5
+ * surface: one rule, two places, asserted rather than trusted. The scaffold's
6
+ * `system/gateways/content.ts` is this file byte-for-byte below the import
7
+ * block, and `default-gateway-drift.test.ts` fails on the line that diverges.
8
+ *
9
+ * Two places rather than one is forced, not chosen. Node refuses to type-strip
10
+ * any `.ts` under `node_modules` — `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`,
11
+ * with no flag to lift it — so the published package cannot import its own
12
+ * emitted template as a fallback. The compiled twin is how a deleted file still
13
+ * serves, and the drift test is what stops the twins disagreeing.
14
+ *
15
+ * Everything below the import block is what an adopter owns: tool names, titles,
16
+ * what the record says it covers, input schemas, annotations, and which tools
17
+ * exist at all. What it composes — handlers, output schemas, and the FLOOR text
18
+ * — stays in the package, because those are the citation and abstention
19
+ * guarantees and a hand-written handler is the one thing no shape check catches.
20
+ */
21
+
22
+ import {
23
+ composeInstructions,
24
+ FLOOR,
25
+ MAX_OUTLINE_LIMIT,
26
+ MAX_SEARCH_K,
27
+ McpServer,
28
+ outlineHandler,
29
+ OUTLINE_OUTPUT,
30
+ READ_ONLY,
31
+ READ_OUTPUT,
32
+ readHandler,
33
+ SEARCH_OUTPUT,
34
+ searchHandler,
35
+ z,
36
+ type ServiceContext,
37
+ } from "@panaversity/ksor/gateway";
38
+
39
+ export default function buildGateway(ctx: ServiceContext, version: string): McpServer {
40
+ const server = new McpServer(
41
+ // The MCP server name agents see. Change it to your record's name.
42
+ { name: "ksor", version },
43
+ // instance.md's body is this record's system prompt, preserved beneath the
44
+ // framework floor. Leave this alone unless you mean to replace the prompt.
45
+ { instructions: composeInstructions(ctx.instance.instructions) },
46
+ );
47
+
48
+ server.registerTool(
49
+ "search",
50
+ {
51
+ title: "Search the record",
52
+ // WHAT THIS RECORD COVERS goes first — it is how an agent with several
53
+ // records attached decides to ask yours. Say the subject AND the
54
+ // boundary; the second half prevents more wrong calls than the first:
55
+ //
56
+ // description: `Employee handbook: leave, benefits, conduct, expenses.
57
+ // Not product documentation and not customer data.\n\n${FLOOR.search}`,
58
+ //
59
+ // FLOOR.search must stay. It tells an agent how to read an abstention and
60
+ // that corpus text is untrusted; the door checks it is still there at boot.
61
+ description: FLOOR.search,
62
+ inputSchema: z.object({
63
+ query: z
64
+ .string()
65
+ .min(1)
66
+ .max(2000)
67
+ .describe("A focused question or phrase to search the record for"),
68
+ // `k` is the lever on reply size: 10 costs an agent ~3,500 tokens a
69
+ // call, 5 costs ~2,000. Lower it to what your record actually needs —
70
+ // a caller can always ask for more.
71
+ k: z
72
+ .number()
73
+ .int()
74
+ .min(1)
75
+ .max(MAX_SEARCH_K)
76
+ .default(10)
77
+ .describe(`Maximum passages to return (1–${MAX_SEARCH_K})`),
78
+ }),
79
+ outputSchema: SEARCH_OUTPUT,
80
+ annotations: READ_ONLY,
81
+ },
82
+ searchHandler(ctx),
83
+ );
84
+
85
+ // Delete a tool by deleting its block. Measured: outline and read together
86
+ // cost ~1,643 tokens of context that is resident for an agent's whole
87
+ // session, whether or not it ever calls them.
88
+ server.registerTool(
89
+ "outline",
90
+ {
91
+ title: "Outline the record",
92
+ description: FLOOR.outline,
93
+ inputSchema: z.object({
94
+ node: z
95
+ .string()
96
+ .optional()
97
+ .describe("Slug or '/'-path to drill into; omit to browse the top level"),
98
+ depth: z.number().int().min(0).max(5).optional().describe("Extra levels below the anchor"),
99
+ limit: z
100
+ .number()
101
+ .int()
102
+ .min(1)
103
+ .max(MAX_OUTLINE_LIMIT)
104
+ .default(200)
105
+ .describe("Maximum rows in ONE page"),
106
+ offset: z
107
+ .number()
108
+ .int()
109
+ .min(0)
110
+ .optional()
111
+ .describe("Rows to skip — pass the previous response's next_offset to continue"),
112
+ }),
113
+ outputSchema: OUTLINE_OUTPUT,
114
+ annotations: READ_ONLY,
115
+ },
116
+ outlineHandler(ctx),
117
+ );
118
+
119
+ server.registerTool(
120
+ "read",
121
+ {
122
+ title: "Read a document",
123
+ description: FLOOR.read,
124
+ inputSchema: z.object({
125
+ slug: z.string().min(1).describe("The document's slug or '/'-qualified path (see outline)"),
126
+ heading: z
127
+ .string()
128
+ .optional()
129
+ .describe(
130
+ "Restrict to one section subtree: a full heading path, any prefix of one, or a " +
131
+ "section's last segment when it is unique in the document",
132
+ ),
133
+ from_heading: z
134
+ .string()
135
+ .optional()
136
+ .describe("Window cursor from a previous response's next"),
137
+ snapshot_token: z
138
+ .string()
139
+ .optional()
140
+ .describe(
141
+ 'The "token" string from a search response\'s "snapshot" object — not the object.',
142
+ ),
143
+ token_budget: z
144
+ .number()
145
+ .int()
146
+ .min(100)
147
+ .max(70000)
148
+ .optional()
149
+ .describe("Response size budget in tokens (default 70000)"),
150
+ }),
151
+ outputSchema: READ_OUTPUT,
152
+ annotations: READ_ONLY,
153
+ },
154
+ readHandler(ctx),
155
+ );
156
+
157
+ // Add your own tools here with ordinary registerTool + zod. They are yours;
158
+ // ksor makes no provenance claim about a tool it did not hand you a handler
159
+ // for, and the boot check only inspects the ones it did.
160
+
161
+ return server;
162
+ }
@@ -1,8 +1,26 @@
1
1
  {
2
2
  "$schema": "https://openapi.vercel.sh/vercel.json",
3
- "framework": null,
4
- "installCommand": "pnpm install --no-frozen-lockfile",
5
- "buildCommand": "pnpm build",
6
- "outputDirectory": "system/site/out",
7
- "trailingSlash": true
3
+ "services": {
4
+ "site": {
5
+ "root": ".",
6
+ "installCommand": "pnpm install --no-frozen-lockfile",
7
+ "buildCommand": "pnpm build",
8
+ "outputDirectory": "system/site/out"
9
+ },
10
+ "door": {
11
+ "root": ".",
12
+ "runtime": "container",
13
+ "entrypoint": "Dockerfile"
14
+ }
15
+ },
16
+ "rewrites": [
17
+ { "source": "/mcp(.*)", "destination": { "service": "door" } },
18
+ {
19
+ "source": "/.well-known/oauth-protected-resource(.*)",
20
+ "destination": { "service": "door" }
21
+ },
22
+ { "source": "/health", "destination": { "service": "door" } },
23
+ { "source": "/ready", "destination": { "service": "door" } },
24
+ { "source": "/(.*)", "destination": { "service": "site" } }
25
+ ]
8
26
  }