@jimhoyd/urlcode 0.5.0 → 0.5.5
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 +13 -16
- package/dist/BUILD-MANIFEST.json +15 -12
- package/dist/authoring.js +50 -9
- package/dist/cli.js +20 -7
- package/dist/ecosystem-cli.js +6 -0
- package/dist/explain-cli.js +1 -1
- package/dist/explain.js +2 -2
- package/dist/extension-artifacts.js +10 -23
- package/dist/extension-bundles.js +8 -16
- package/dist/extension-transport.js +41 -0
- package/dist/feature-plan.js +99 -0
- package/dist/index.js +2 -2
- package/dist/mcp.js +6 -2
- package/dist/review.js +206 -0
- package/dist/router.js +15 -2
- package/dist/tooling.js +4 -0
- package/dist/types/authoring.d.ts +2 -0
- package/dist/types/explain.d.ts +3 -0
- package/dist/types/extension-artifacts.d.ts +2 -4
- package/dist/types/extension-transport.d.ts +31 -0
- package/dist/types/feature-plan.d.ts +67 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/review.d.ts +30 -0
- package/dist/types/tooling.d.ts +4 -0
- package/dist/types/types.d.ts +9 -1
- package/dist/types.js +9 -2
- package/docs/AI-AUTHORING.md +466 -0
- package/docs/FUNCTION-SECURITY.md +251 -0
- package/docs/README.md +96 -0
- package/docs/TOOLING.md +422 -0
- package/docs/YAML-REFERENCE.md +473 -0
- package/llms-full.txt +114 -79
- package/llms.txt +28 -19
- package/package.json +9 -3
- package/recipes/store-crud/README.md +9 -10
- package/recipes/store-crud/recipe.yaml +1 -1
- package/schemas/urlcode.schema.json +3 -0
package/docs/TOOLING.md
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
# Tooling SDK and local MCP
|
|
2
|
+
|
|
3
|
+
The tooling API consolidates authoring operations without starting a runtime:
|
|
4
|
+
|
|
5
|
+
- `inspectProject(project, {origin?, target?, offset?, limit?})` loads and
|
|
6
|
+
semantically compiles the project and returns route metadata, revision hash and
|
|
7
|
+
target compatibility. Route pages default to 100 entries, maximum 1,000.
|
|
8
|
+
Compatibility contains global `compatible`, `requirementCount` and `issueCount`
|
|
9
|
+
plus a separate `issues` page. Both arrays use the same zero-based `offset`
|
|
10
|
+
and `limit`, independently: an issue page is indexed over all compatibility
|
|
11
|
+
issues, not filtered to the route page. Compatibility includes `hasMore` for
|
|
12
|
+
its issue page. Empty pages never imply compatibility; the verdict and counts
|
|
13
|
+
always cover the entire project. Full requirement arrays are omitted because
|
|
14
|
+
route entries already contain their capability names.
|
|
15
|
+
- `validateProject(project, options)` returns the same compilation verdict and
|
|
16
|
+
compatibility verdict and global counts without route or issue pages.
|
|
17
|
+
`firstIssue` is the first issue across the whole project, or null; supplied
|
|
18
|
+
pagination settings do not change it. The low-level
|
|
19
|
+
`analyzeCompiledCapabilities` API still returns the complete report.
|
|
20
|
+
For unusually long route paths, request smaller pages to fit the MCP output
|
|
21
|
+
byte limit; page entry bounds do not override that transport limit.
|
|
22
|
+
- `explainRoute(project, path, options)` selects the route for a path and
|
|
23
|
+
describes its effective behavior from the compiled IR: methods, handler (with
|
|
24
|
+
its destination, module and export, file, extension or link collection),
|
|
25
|
+
the middleware chain in order, validated inputs (parameters and the request
|
|
26
|
+
body policy), the policies in effect with each compiled policy's inventory,
|
|
27
|
+
extension requirements, the cache outcome (the policy strategy, an explicit
|
|
28
|
+
header, an asset declaration, or the `no-store` the runtime forces on
|
|
29
|
+
extension, proxy and conditional routes), binding names (never values),
|
|
30
|
+
egress origins, response headers, capabilities and per-target support. A miss
|
|
31
|
+
returns `matched: false` with the nearest route patterns. With `extensions`
|
|
32
|
+
(a host file's registrations) each extension requirement also reports whether
|
|
33
|
+
a provider is registered, whether its revision pin matches and whether the
|
|
34
|
+
requirement satisfies the provider's policy schema; nothing is activated.
|
|
35
|
+
`explainProject(project, options)` returns every route the same way.
|
|
36
|
+
- `buildManifest(project, options)` returns the generated semantic manifest
|
|
37
|
+
described under [`urlcode manifest`](#explain-and-manifest).
|
|
38
|
+
- `getCapabilities(target?)` describes local implementation support and separate
|
|
39
|
+
deployment evidence.
|
|
40
|
+
- `getCapability(name)` returns one catalog entry: kind, summary, resolved schema
|
|
41
|
+
fragments, constraints, required operator grants, per-target support, refused
|
|
42
|
+
targets and the bundled recipes and cookbook routes that use it. Unknown names
|
|
43
|
+
throw a `ConfigError` listing the valid names.
|
|
44
|
+
- `getSchemaFragment(path)` returns only the fragment of
|
|
45
|
+
`schemas/urlcode.schema.json` for a dotted path (`route`, `redirect`,
|
|
46
|
+
`policies.cache`, `site.sitemap`) with local `$ref`s inlined; `schemaPathNames()`
|
|
47
|
+
lists the accepted top-level names. Both read bundled package data only.
|
|
48
|
+
- `previewImport(options)` and `previewExport(project, format, acknowledgment?)`
|
|
49
|
+
return conversion reports and candidate text, never writing files. Provider
|
|
50
|
+
semantic differences require the existing explicit acknowledgment and remain
|
|
51
|
+
non-lossless.
|
|
52
|
+
- `listRecipes()` and `showRecipe(name)` expose the fixed bundled recipe catalog.
|
|
53
|
+
- `inspectExtensions({project, hostFile?})` reports each operator-registered
|
|
54
|
+
extension's name, contract version, targets, credential headers, configuration
|
|
55
|
+
and policy JSON Schemas, machine-readable project hook contracts, whether the project declares it, whether its revision
|
|
56
|
+
pin matches and where routes mount or require it, plus the project's declared
|
|
57
|
+
names. With `hostFile` it executes that trusted operator module under the
|
|
58
|
+
`--host-file` rules (absolute path, outside the project) and releases it
|
|
59
|
+
afterwards; without one it lists declarations only. `describeExtensions(project,
|
|
60
|
+
registrations?)` produces the same report from registrations already in hand.
|
|
61
|
+
Neither activates an extension. See [EXTENSIONS.md](EXTENSIONS.md).
|
|
62
|
+
- `buildContext(project, {target?, hostFile?, budget?})` returns the compact
|
|
63
|
+
project context an authoring agent needs before it writes anything (see
|
|
64
|
+
below); `renderContext` produces the YAML rendering and `estimateTokens`
|
|
65
|
+
the characters-per-token estimate the budget uses.
|
|
66
|
+
|
|
67
|
+
## Project context
|
|
68
|
+
|
|
69
|
+
`urlcode context [--project DIR] [--target T] [--host-file F] [--budget N]
|
|
70
|
+
[--json] [--stats]` emits one deterministic YAML document (JSON with
|
|
71
|
+
`--json`) derived only from the compiled project and the capability catalog,
|
|
72
|
+
never from prose. It uses the same loader and semantic compiler as
|
|
73
|
+
`inspectProject`: no binding values, guest execution, environment reads or
|
|
74
|
+
network. Keys always appear in this order:
|
|
75
|
+
|
|
76
|
+
- `urlcode` (package version) and `schema` (`"1"`).
|
|
77
|
+
- `project`: entry file, route count, handlers used with counts, extensions
|
|
78
|
+
declared, policies in effect at the top level and the number of routes each
|
|
79
|
+
policy applies to, requested env and secret binding names,
|
|
80
|
+
`site` keys, and `files` (include, function and middleware paths). With
|
|
81
|
+
`--host-file`, `host` counts the operator module's extensions and plugins
|
|
82
|
+
without activating them.
|
|
83
|
+
- `routes`: path, methods and handler per route, sorted by path.
|
|
84
|
+
- `constraints`: a fixed list that holds for every project (network and Node
|
|
85
|
+
built-ins available to trusted code and withdrawn by `sandbox: true`, no
|
|
86
|
+
regex routes, one handler per route, exact or `{param}` path segments,
|
|
87
|
+
subtree mounts only for static and extension routes, no YAML interpolation,
|
|
88
|
+
injected `env`/`secrets` by operator grant only), each with a value and a
|
|
89
|
+
note spelling out how it differs between the two trust modes.
|
|
90
|
+
- `targets`: for each capability target (or the one `--target`), which of this
|
|
91
|
+
project's used features are supported, conditional, refused or unknown.
|
|
92
|
+
- `commands`: the exact `validate`, `test`, `audit --expect-routes N` (N is
|
|
93
|
+
the compiled route count), `routes` and `capabilities` invocations.
|
|
94
|
+
|
|
95
|
+
`--budget N` drops sections in a fixed order until the YAML rendering fits
|
|
96
|
+
the estimate: per-route detail, then `targets`, then the constraint notes
|
|
97
|
+
(keys and values stay), then `project.files`, then `commands`. The dropped
|
|
98
|
+
sections are listed under `omitted`. The estimate is `ceil(characters / 4)`;
|
|
99
|
+
there is no tokenizer dependency, so treat both numbers as approximate. A
|
|
100
|
+
budget the smallest rendering cannot meet is an error rather than an
|
|
101
|
+
overrun. `--stats` writes a JSON line to stderr comparing the estimated size
|
|
102
|
+
of the shipped documentation (`docs/*.md` and `llms.txt`) with the emitted
|
|
103
|
+
context, labeled `estimate: characters/4`. The MCP tool `get_context` takes
|
|
104
|
+
`target` and `budget` and returns the same object with `--project .` in the
|
|
105
|
+
commands; it never takes a host file or any other path.
|
|
106
|
+
|
|
107
|
+
Inspection reads declared configuration and function source graphs to validate
|
|
108
|
+
references and compute revision hashes. It compiles route and policy semantics
|
|
109
|
+
using dummy binding values. It never reads environment or dotenv credentials,
|
|
110
|
+
starts guest execution, follows network destinations, or opens operator link
|
|
111
|
+
stores. The result contains no raw compiled route, binding values or source text.
|
|
112
|
+
Inspection is not deployment readiness: missing operator grants, live service
|
|
113
|
+
availability, asset snapshot activation and provider behavior require their own
|
|
114
|
+
checks. Build output remains an explicit separate build API/CLI operation.
|
|
115
|
+
|
|
116
|
+
## Feature planning
|
|
117
|
+
|
|
118
|
+
After `get_context`, use `urlcode plan-feature "goal" --project DIR --target
|
|
119
|
+
self-hosted --json` (MCP `plan_feature {goal, target?}`) when the next question
|
|
120
|
+
is which already-supported contract applies. It returns a bounded structured
|
|
121
|
+
plan: matching local recipes and capability decisions for the current revision,
|
|
122
|
+
operator-owned extension prerequisites and their registration/target status,
|
|
123
|
+
inert locked-artifact status, a deliberately small route/config outline where a
|
|
124
|
+
recipe defines one, application-code boundaries, explicit gaps, and the next
|
|
125
|
+
bounded calls. It never returns generated application code.
|
|
126
|
+
|
|
127
|
+
The goal is a 1–512 character string reduced to at most sixteen normalized
|
|
128
|
+
terms; the returned JSON is capped at 32 KiB (an estimated token count is
|
|
129
|
+
included). It only uses the compiled project, packaged capability/recipe data,
|
|
130
|
+
the already-verified local artifact cache and registrations that the operator
|
|
131
|
+
already supplied to the CLI/MCP session. It does not open a host file itself,
|
|
132
|
+
read binding values, execute guest or extension code, fetch a service, or make
|
|
133
|
+
a project change. A locked schema artifact remains inert and a registered
|
|
134
|
+
extension remains an operator decision: neither lets YAML select a package,
|
|
135
|
+
storage provider, key or grant. Canonical extension ordering is resolved only
|
|
136
|
+
by the operator-approved composition/scaffold contract, not by this planner.
|
|
137
|
+
|
|
138
|
+
The package root also exports existing operator-invoked workflow APIs:
|
|
139
|
+
`buildCloudflare(project, options)` compiles and writes a Cloudflare artifact;
|
|
140
|
+
`buildStatic(project, options)` compiles redirects and static files into plain
|
|
141
|
+
objects and redirect metadata for S3 + CloudFront (see [static
|
|
142
|
+
hosting](STATIC.md)); `runProjectTests(project, options)` starts the local
|
|
143
|
+
runtime, executes request fixtures and closes it; `scaffoldProject(project,
|
|
144
|
+
{dryRun})` creates missing placeholders while preserving existing files;
|
|
145
|
+
`initProject(destination)` creates the standard starter; and
|
|
146
|
+
`addRedirect(project, destination, alias?)` updates project YAML under the
|
|
147
|
+
authoring lock. `CloudflareBuildOptions`, `CloudflareBuildReport`,
|
|
148
|
+
`StaticBuildOptions`, `StaticBuildReport`, `ProjectTestOptions`,
|
|
149
|
+
`ProjectTestResult`, `ScaffoldReport` and `ScaffoldUnresolved` describe these
|
|
150
|
+
existing operations.
|
|
151
|
+
|
|
152
|
+
These SDK functions have explicit write or execution effects and are available
|
|
153
|
+
to trusted callers only. Project tests use normal runtime activation, grants and
|
|
154
|
+
sandboxing; granted proxy/signal fixtures can perform real outbound operations.
|
|
155
|
+
Compilation and authoring write caller-selected destinations under each existing
|
|
156
|
+
helper's documented rules. They are **not** MCP tools. MCP remains limited to
|
|
157
|
+
the read-only operations below; adding a package-root export does not grant an
|
|
158
|
+
assistant file-write, guest-execution, deployment or network authority.
|
|
159
|
+
|
|
160
|
+
## Project review
|
|
161
|
+
|
|
162
|
+
`urlcode review [--project DIR] [--target T] [--host-file F] [--json]` (MCP
|
|
163
|
+
`review_project {target?}`) is an opt-in, read-only static review of the
|
|
164
|
+
compiled project plus its own `function`/`middleware` source, for the narrow,
|
|
165
|
+
agent-facing question "which of this generated code looks like avoidable
|
|
166
|
+
framework plumbing, and what is the supported alternative?" It scans only the
|
|
167
|
+
project's own root-confined source graph (the same `function`/`middleware`
|
|
168
|
+
file resolution `explain` and `manifest` use): no project code is executed, no
|
|
169
|
+
environment variable or secret is read, and no network call is made. Findings
|
|
170
|
+
are grouped:
|
|
171
|
+
|
|
172
|
+
- `native-alternative`: an already-supported declarative capability appears to
|
|
173
|
+
cover the behavior (for example `request.body.schema` in place of
|
|
174
|
+
hand-written `JSON.parse` plus field checks, or one route per method in
|
|
175
|
+
place of a hand-written `request.method` dispatch table).
|
|
176
|
+
- `extension-alternative`: the project **declares** an extension that could
|
|
177
|
+
plausibly own the behavior. Without `--host-file`, the required operator
|
|
178
|
+
setup (registration, revision pin) is stated as unconfirmed — a declaration
|
|
179
|
+
is never reported as an active or executable extension. With `--host-file`,
|
|
180
|
+
the already-loaded operator registrations (the same ones `explain` and
|
|
181
|
+
`plan-feature` accept; the host file is trusted operator code outside the
|
|
182
|
+
project, never project code, and review only reads the registrations it
|
|
183
|
+
already returned — it still never activates or calls into an extension)
|
|
184
|
+
sharpen the finding to state whether that specific extension is actually
|
|
185
|
+
registered and, if so, whether the registration is pinned to this project's
|
|
186
|
+
current revision (`registered`/`revisionPinned` on the observation). A
|
|
187
|
+
registered extension is still never reported as active or executable —
|
|
188
|
+
only as registered, which is a narrower, verifiable claim.
|
|
189
|
+
- `gap`: no current native or extension composition covers the pattern (for
|
|
190
|
+
example durable, cross-instance counters); this is reported as a real
|
|
191
|
+
capability gap, not a mistake to silently patch.
|
|
192
|
+
- `manual-review`: a security- or durable-state-sensitive pattern (manually
|
|
193
|
+
assembled cookies/sessions, a direct outbound network call, or hand-written
|
|
194
|
+
logic that duplicates a `policies.*` block already declared for the route)
|
|
195
|
+
that this tool never classifies automatically. Trusted, unsandboxed
|
|
196
|
+
execution is an explicit supported mode (`SPIKE-DEFAULT-TRUST-MODEL.md`);
|
|
197
|
+
nothing here claims a function is unsafe solely because it is trusted.
|
|
198
|
+
|
|
199
|
+
Its scope covers seven signals, each with source location, a short bounded
|
|
200
|
+
excerpt (untrusted project text, never executed or treated as instructions), a
|
|
201
|
+
confidence level and a plain-language reason:
|
|
202
|
+
|
|
203
|
+
- Hand-written JSON body validation (`manual-body-validation`).
|
|
204
|
+
- Manually assembled `Set-Cookie`/session construction (`manual-cookie-session`).
|
|
205
|
+
- Module-scope mutable state later mutated in the same file (`global-mutable-state`).
|
|
206
|
+
- A direct outbound call, `fetch`/`http(s).request`/`http(s).get` (`outbound-network-call`).
|
|
207
|
+
- Hand-written `request.method` branching or a `switch (request.method)`
|
|
208
|
+
dispatch table (`method-dispatch`), reported as `native-alternative`:
|
|
209
|
+
declaring one route per method is the native alternative (see
|
|
210
|
+
`get_capability("methods")`); URLCode has no per-method-function YAML shape
|
|
211
|
+
to point at instead (`YAML-REFERENCE.md` is explicit that "a `methods:` map
|
|
212
|
+
of per-method functions is not implemented").
|
|
213
|
+
- Hand-rolled request counting paired with a `429`/`Retry-After` response
|
|
214
|
+
(`manual-rate-limit`). Reported as `native-alternative` (pointing at
|
|
215
|
+
`get_capability("policies.throttle")`) when `policies.throttle` is not
|
|
216
|
+
effectively declared for the route, or as `manual-review` when it already
|
|
217
|
+
is — duplicating an active policy is a real conflict, not just a missed
|
|
218
|
+
opportunity, and needs a human decision to remove one side.
|
|
219
|
+
- Two or more hand-set security response headers, from `X-Frame-Options`,
|
|
220
|
+
`Content-Security-Policy`, `Strict-Transport-Security`,
|
|
221
|
+
`X-Content-Type-Options`, `Referrer-Policy`, `Permissions-Policy` or
|
|
222
|
+
`X-XSS-Protection` (`manual-security-headers`). Reported as
|
|
223
|
+
`native-alternative` (pointing at `get_capability("policies.security")`)
|
|
224
|
+
when `policies.security` is not effectively declared for the route, or as
|
|
225
|
+
`manual-review` when it already is, for the same reason as rate limiting.
|
|
226
|
+
|
|
227
|
+
Both policy-duplication signals cross-reference each route's actual *effective*
|
|
228
|
+
policy (project/profile defaults plus the route's own `policies` block, the
|
|
229
|
+
same resolution `compilePolicies` performs) before deciding whether to word a
|
|
230
|
+
finding as "you could declare this" (`native-alternative`) or "this duplicates
|
|
231
|
+
what's already declared" (`manual-review`) — never the reverse, and never a
|
|
232
|
+
claim of duplication against a policy that was never declared for that route.
|
|
233
|
+
|
|
234
|
+
It is deliberately conservative and does not attempt every signal a generated
|
|
235
|
+
project could exhibit. Hand-written conditional redirect logic (branching in
|
|
236
|
+
code toward what could be a declarative `match`/`conditional` route) was
|
|
237
|
+
considered and set aside: ordinary application branching that happens to end
|
|
238
|
+
in a redirect is common and mostly has nothing to do with routing
|
|
239
|
+
configuration, so a bounded source-text signal for it would be prone to
|
|
240
|
+
false positives against legitimate business logic — an uncertain observation
|
|
241
|
+
is preferable to an incorrect automatic refactor.
|
|
242
|
+
|
|
243
|
+
## Explain and manifest
|
|
244
|
+
|
|
245
|
+
`urlcode explain [/route] [--project DIR] [--target T] [--host-file F] [--json]`
|
|
246
|
+
prints what `explainRoute` returns: one route in detail, or without a path a
|
|
247
|
+
one-line-per-route table (methods, handler, state, execution mode, middleware
|
|
248
|
+
count, policies, cache outcome and target support). `--target` narrows the support columns to
|
|
249
|
+
one deployment target; `--host-file` supplies the operator registry so
|
|
250
|
+
extension requirements show their provider. An unknown route exits 1 and names
|
|
251
|
+
the nearest patterns. Everything comes from the compiled configuration: no
|
|
252
|
+
request is evaluated, no function runs and no binding is read.
|
|
253
|
+
|
|
254
|
+
`urlcode manifest [--project DIR] [--json]` emits the semantic manifest:
|
|
255
|
+
`schemaVersion`, the `urlcode` version, the entry file and its includes, the
|
|
256
|
+
`revision` (the same digest `inspectExtensionRevision` returns, so an operator
|
|
257
|
+
pin can be checked against it), the config `configVersion`, every route (path,
|
|
258
|
+
methods, handler, state, execution mode (`sandbox`, with `sandboxReason` when
|
|
259
|
+
the route declares one), middleware, inputs, policy names, extension
|
|
260
|
+
requirements, cache outcome, binding names, egress origins, capabilities and
|
|
261
|
+
per-target support), the union of capabilities used, extension declarations
|
|
262
|
+
(version, configuration keys, mounts and protected routes), recipe provenance
|
|
263
|
+
(from a `recipe.yaml` beside the entry file when one exists), external
|
|
264
|
+
requirements (environment and secret names, proxy and signal origins,
|
|
265
|
+
extensions), the function and middleware
|
|
266
|
+
modules with the routes that use them, and per-target compatibility. Without
|
|
267
|
+
`--json` a short summary prints. The manifest is deterministic: the same
|
|
268
|
+
project produces the same bytes. `urlcode build` writes the same document as
|
|
269
|
+
`manifest.json` beside its output, and `buildManifest` returns it from the SDK.
|
|
270
|
+
It is generated output, never a checked-in source of truth; regenerate it
|
|
271
|
+
rather than editing it.
|
|
272
|
+
|
|
273
|
+
`serveMcp({project, input?, output?, origin?, allowAuthoring?, hostFile?})` serves one
|
|
274
|
+
operator-selected root on stdio. Its tools are `inspect`, `validate`,
|
|
275
|
+
`capabilities`, `get_capability`, `get_schema`, `explain`, `get_manifest`,
|
|
276
|
+
`import_preview`, `export_preview`, `recipes_list`, `recipes_show`,
|
|
277
|
+
`search_recipes`, `search_examples`, `list_skills`, `get_skill`, `search_docs`,
|
|
278
|
+
`get_example`, `validate_yaml`, `explain_error`, `get_extension_artifacts`,
|
|
279
|
+
`get_extension_artifact`, `get_context`, `plan_feature` and `review_project`. The skill,
|
|
280
|
+
documentation and example tools read only a fixed package-owned manifest; no
|
|
281
|
+
tool argument names an arbitrary local path or remote URL. The CLI equivalent of `search_docs` is
|
|
282
|
+
`urlcode docs search TEXT [--json]`, which returns the same at most three bounded excerpts. `validate_yaml` checks supplied
|
|
283
|
+
YAML syntax and schema only, while `validate` compiles the selected local project.
|
|
284
|
+
`get_extension_artifacts` validates the project-selected
|
|
285
|
+
`urlcode.extensions.lock.json` and cache, then returns artifact metadata,
|
|
286
|
+
status and allowlisted member paths. `get_extension_artifact` accepts only a
|
|
287
|
+
locked artifact name and one of those relative JSON/Markdown member paths; it
|
|
288
|
+
revalidates the cache and reads at most 512 KiB directly from the signed archive.
|
|
289
|
+
Both are local, read-only and inert: they never download, install, update or
|
|
290
|
+
activate an extension and never substitute for `get_extensions`, which reports
|
|
291
|
+
the operator-registered executable contract.
|
|
292
|
+
When the operator starts
|
|
293
|
+
the server with `--host-file`, it loads that trusted module once for the session
|
|
294
|
+
and additionally advertises `get_extensions`, which returns the
|
|
295
|
+
`inspectExtensions` report; without the option the tool is absent and calls to
|
|
296
|
+
it are rejected. Tools accept no project/file/output path argument; recipe names
|
|
297
|
+
come from the fixed catalog, `get_capability` names from the capability catalog,
|
|
298
|
+
`get_schema` paths from the bundled schema, and the two searches match bundled
|
|
299
|
+
metadata locally (see [recipes](RECIPES.md)).
|
|
300
|
+
There is no shell, arbitrary file read, remote fetch, binding access, write or
|
|
301
|
+
route-execution tool without the explicit [authoring mode](#authoring-mode) flag. Configuration includes and module references retain the
|
|
302
|
+
runtime's existing root containment checks. Returned project and recipe content
|
|
303
|
+
is data, not trusted instructions for the consuming agent.
|
|
304
|
+
|
|
305
|
+
The server implements the MCP **2025-11-25** lifecycle and stdio framing. Clients
|
|
306
|
+
initialize, verify the returned protocol version, then send
|
|
307
|
+
`notifications/initialized` before tool operations. Other requested revisions
|
|
308
|
+
negotiate to this explicit supported version; a client that cannot support it
|
|
309
|
+
must disconnect. Newer lifecycle revisions are not claimed. Requests use UTF-8
|
|
310
|
+
newline-delimited JSON-RPC 2.0, with one request at a time and stream backpressure.
|
|
311
|
+
There is a 1 MiB input-frame and output-message limit; oversized input terminates
|
|
312
|
+
the session after a fixed error, and truncated/invalid frames return protocol
|
|
313
|
+
errors. Import text is additionally capped at 512 KiB. Tool schemas reject
|
|
314
|
+
unknown arguments. Tool operation errors are generic to avoid exposing local
|
|
315
|
+
source paths, credentials or configuration excerpts; inspect locally for details.
|
|
316
|
+
|
|
317
|
+
## Registering the server
|
|
318
|
+
|
|
319
|
+
`urlcode init` (and `init --with`) writes `.mcp.json` at the project root, the
|
|
320
|
+
shape Claude Code and Codex read:
|
|
321
|
+
|
|
322
|
+
```json
|
|
323
|
+
{ "mcpServers": { "urlcode": { "command": "urlcode", "args": ["mcp", "--project", "."] } } }
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
For an `init --with` site the file sits beside `host.mjs` and passes
|
|
327
|
+
`--project app`. An existing `.mcp.json` is never overwritten. The file registers
|
|
328
|
+
the read-only server only: `--allow-authoring` (and `--host-file`) are operator
|
|
329
|
+
choices added by hand, never by `init` or by an agent.
|
|
330
|
+
|
|
331
|
+
- **Claude Code** reads `.mcp.json` in the project directory as a project-scoped
|
|
332
|
+
server and asks for approval on first use. A project that pins the runtime in its
|
|
333
|
+
`package.json` (`--template redirects`, `--with`, `--manifest`) gets
|
|
334
|
+
`"command": "npx"` with `--no --package @jimhoyd/urlcode urlcode mcp ...`, which runs the
|
|
335
|
+
installed copy and never fetches (do not use a bare `npx urlcode`: that names an unrelated
|
|
336
|
+
registry package). A project without one keeps the bare `urlcode` command for a global
|
|
337
|
+
install; for a local-only install replace it with `"node"` and prefix the arguments with
|
|
338
|
+
`node_modules/@jimhoyd/urlcode/dist/cli.js`.
|
|
339
|
+
- **Codex** reads the same `mcpServers` shape; alternatively register it in
|
|
340
|
+
`~/.codex/config.toml`:
|
|
341
|
+
|
|
342
|
+
```toml
|
|
343
|
+
[mcp_servers.urlcode]
|
|
344
|
+
command = "urlcode"
|
|
345
|
+
args = ["mcp", "--project", "."]
|
|
346
|
+
```
|
|
347
|
+
- **Any stdio client** spawns `urlcode mcp --project DIR` with the project as the
|
|
348
|
+
working directory, speaks newline-delimited JSON-RPC 2.0 over stdin/stdout,
|
|
349
|
+
and follows the 2025-11-25 lifecycle described above. Nothing listens on a
|
|
350
|
+
port; closing stdin ends the session.
|
|
351
|
+
|
|
352
|
+
The generated `AGENTS.md` and the packaged skill tell agents to prefer
|
|
353
|
+
`get_context`, `get_capability`, `get_schema`, `search_recipes`, `explain` and
|
|
354
|
+
`get_manifest` when the server is registered and to fall back to the matching
|
|
355
|
+
CLI commands otherwise.
|
|
356
|
+
|
|
357
|
+
## Authoring mode
|
|
358
|
+
|
|
359
|
+
`urlcode mcp --allow-authoring --project DIR` adds six tools to the twenty-two read
|
|
360
|
+
tools above. The flag is honored from the operator's command line only: no
|
|
361
|
+
tool argument, environment variable or client capability enables it, and
|
|
362
|
+
without it the server is exactly the read-only server described above.
|
|
363
|
+
|
|
364
|
+
What it can do, all inside the selected project root (resolved with realpath):
|
|
365
|
+
|
|
366
|
+
- `create_route {path, handler, middleware?, file?}` adds one route to
|
|
367
|
+
`urlcode.yaml` or to an include listed in it. `handler` is a route object
|
|
368
|
+
(`{redirect: {...}}`, `{function: {...}}`, `{page: {...}}`, ...) or a short
|
|
369
|
+
form: an `http(s)://` URL becomes a redirect, a `.js`/`.mjs` path becomes a
|
|
370
|
+
function whose `{param}` path segments expand to required bounded string
|
|
371
|
+
parameters and matching `args`. `middleware` entries are sources or objects.
|
|
372
|
+
The merged project is checked before the write (schema, duplicate routes,
|
|
373
|
+
the `auth` short form, and the same reference compilation `urlcode add`
|
|
374
|
+
performs when every referenced source exists). The edit runs under the
|
|
375
|
+
authoring lock and replaces the file atomically. Missing sources are listed
|
|
376
|
+
in `missingSources` for `scaffold_feature`.
|
|
377
|
+
- `add_recipe {name, destination, dryRun?}` runs `recipes add` into a new
|
|
378
|
+
directory under the project. The parent must exist; an existing destination
|
|
379
|
+
is refused, never merged. `dryRun` reports the destination and writes nothing.
|
|
380
|
+
- `scaffold_feature {dryRun?}` runs `urlcode scaffold`: placeholder modules,
|
|
381
|
+
pages and directories for references the YAML makes and the disk lacks.
|
|
382
|
+
Existing files are preserved, never overwritten.
|
|
383
|
+
- `run_validate`, `run_test`, `run_audit` spawn `urlcode validate --local`,
|
|
384
|
+
`urlcode test` and `urlcode audit` against the project with a minimal
|
|
385
|
+
environment (`PATH` only), a two-minute deadline and stdout/stderr each capped
|
|
386
|
+
at 32 KiB. The result carries `exitCode`, `signal`, `stdout`, `stderr` and
|
|
387
|
+
`truncated`. `run_test` activates the local runtime and executes fixtures,
|
|
388
|
+
under the same rules as the CLI.
|
|
389
|
+
|
|
390
|
+
Every tool returns `validation`, the `validateProject` verdict of the project
|
|
391
|
+
after the operation (or `valid: false` with a generic note; use `run_validate`
|
|
392
|
+
for the CLI report).
|
|
393
|
+
|
|
394
|
+
What it cannot do:
|
|
395
|
+
|
|
396
|
+
- Write outside the project root. Paths are project-relative; absolute paths,
|
|
397
|
+
`..`, backslashes, drive letters, and any symlink on the walk are refused
|
|
398
|
+
before the write, and the recipe, scaffold and `urlcode add` paths keep their
|
|
399
|
+
own containment checks.
|
|
400
|
+
- Touch `.env*`, anything under `.git`, `node_modules`, `package.json`,
|
|
401
|
+
credential files (`.pem`, `.key`, `.p12`, `.pfx`), the authoring lock, or
|
|
402
|
+
operator files by name: `*policy*.json`, `*compliance*`, `host.mjs` /
|
|
403
|
+
`host-file.mjs` and link stores (`.sqlite`, `.db` and their WAL/SHM files).
|
|
404
|
+
Operator files belong outside the checkout in the first place.
|
|
405
|
+
- Create or change grants, read bindings or secret values, deploy, build, run
|
|
406
|
+
arbitrary commands, delete or edit existing files (except the one YAML file a
|
|
407
|
+
`create_route` targets), or serve a project other than the one the operator
|
|
408
|
+
selected.
|
|
409
|
+
|
|
410
|
+
Authoring mode is a local, unauthenticated stdio process for an operator who
|
|
411
|
+
already trusts the assistant to edit this checkout. Review the resulting diff
|
|
412
|
+
as you would any contributor's before running `serve` or deploying.
|
|
413
|
+
|
|
414
|
+
Only tools are advertised. Resources, prompts, subscriptions, sampling,
|
|
415
|
+
elicitation, HTTP transport, cancellation and durable tasks are not implemented.
|
|
416
|
+
Closing stdin ends the session after the current bounded operation. Existing
|
|
417
|
+
configuration-loader and semantic-compiler deadlines still apply. This local
|
|
418
|
+
process is not an authenticated remote service or an independent security review.
|
|
419
|
+
|
|
420
|
+
Protocol references: [MCP stdio transport](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports),
|
|
421
|
+
[MCP lifecycle](https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle),
|
|
422
|
+
and [MCP tools](https://modelcontextprotocol.io/specification/2025-11-25/server/tools).
|