candor-ts 0.36.1 → 0.36.2

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.
Files changed (3) hide show
  1. package/README.md +23 -0
  2. package/package.json +1 -1
  3. package/scan.mjs +41 -1
package/README.md CHANGED
@@ -249,3 +249,26 @@ node scan.mjs <dir | file.ts | tsconfig.json> --out .candor/report # scan a pr
249
249
  The pure cores are factored into importable modules — `query-core.mjs` (the §3.1 queries),
250
250
  `policy.mjs` (the §6.2 DSL + literal matchers), and `scan-core.mjs` (the classifier + the SQL/
251
251
  command/host extractors) — so they're unit-tested directly; the TS-compiler-driven walk stays in `scan.mjs`.
252
+
253
+ ## Agents: install candor as an MCP server
254
+
255
+ `candor-ts` ships candor's read-only query surface as an MCP server, so an agent asks *"what is the
256
+ blast radius of changing this?"* or *"what reaches the network?"* and gets a deterministic answer from a
257
+ precomputed report instead of grepping for it.
258
+
259
+ ```bash
260
+ npx -y candor-ts --mcp # the server, over stdio
261
+ candor mcp install # or let the umbrella write/merge .mcp.json for you
262
+ ```
263
+
264
+ Registration instructions come from **the package you already installed** — `candor mcp --help` prints
265
+ the `.mcp.json` snippet and the `claude mcp add` line, and `candor mcp install` writes them. There is no
266
+ remote file to fetch, tamper with, or auto-execute, and nothing here tells an agent to run anything it
267
+ did not already choose to install.
268
+
269
+ Discovery is passive: the manifest is [`server.json`](server.json), published to the official
270
+ [MCP Registry](https://registry.modelcontextprotocol.io) so clients and directories can find candor
271
+ without being handed a script. The registry verifies namespace ownership through the marker below.
272
+
273
+ mcp-name: io.github.tombaldwin/candor
274
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "candor-ts",
3
- "version": "0.36.1",
3
+ "version": "0.36.2",
4
4
  "description": "candor for TypeScript \u2014 per-function side effects, transitively, with a policy gate (candor-spec 0.36)",
5
5
  "type": "module",
6
6
  "dependencies": {
package/scan.mjs CHANGED
@@ -108,6 +108,27 @@ if (process.argv.includes("--version") || process.argv.includes("-V")) {
108
108
  process.exit(0);
109
109
  }
110
110
 
111
+ // --mcp: a MODE, in the same place and for the same reason as --version above — it hands the process
112
+ // to the MCP server so the ONE bin the MCP Registry can invoke (`npx -y candor-ts`) is able to serve.
113
+ //
114
+ // WHY A FLAG AND NOT A BARE `mcp` SUBCOMMAND, which is what ebman does. This script takes its scan
115
+ // target POSITIONALLY, so `candor-ts mcp` would be ambiguous with a directory literally named `mcp` —
116
+ // and a registry-invoked server runs in whatever repo the client happens to be in, where that
117
+ // directory may well exist. A flag cannot collide with a path. The ambiguity would be rare and silent,
118
+ // which is the combination worth designing out rather than documenting.
119
+ //
120
+ // spawnSync rather than `await import("./mcp.mjs")`: an import would RUN the server and then fall
121
+ // through into this file's own argument walk when it returned. Handing the child our stdio and
122
+ // exiting on its status keeps the two programs separate, which is what the transport needs anyway.
123
+ if (process.argv.includes("--mcp")) {
124
+ const { spawnSync } = await import("node:child_process");
125
+ const { fileURLToPath } = await import("node:url");
126
+ const server = fileURLToPath(new URL("./mcp.mjs", import.meta.url));
127
+ const rest = process.argv.slice(2).filter((a) => a !== "--mcp");
128
+ const r = spawnSync(process.execPath, [server, ...rest], { stdio: "inherit" });
129
+ process.exit(r.status ?? 0);
130
+ }
131
+
111
132
  // -h / --help: a print-and-exit MODE (like --version), handled before the arg walk so `-h` (a single
112
133
  // dash) is never mistaken for the scan target by the positional fallthrough below.
113
134
  if (process.argv.includes("-h") || process.argv.includes("--help")) {
@@ -7143,8 +7164,27 @@ function visitCalls(node) {
7143
7164
  // a connected socket). `post/put/patch/delete/head/options` cover the axios/got/undici tier whose
7144
7165
  // URL is the call arg (sweep [18]); `dgram.send(buf,port,host)` is added module-aware below (UDP
7145
7166
  // has no connect, so send carries the destination — sweep [12]).
7167
+ // SOUNDNESS R410 — THE RESOLVER FAMILY WAS MISSING, and its absence is a GATE BYPASS, not a
7168
+ // missed disclosure. `dns.resolve` classifies Net (see the κ table below), so a resolver call
7169
+ // carries the effect while contributing NO host; with this list not naming it, nothing marked
7170
+ // the surface incomplete and a benign sibling `fetch("https://api.stripe.com")` certified a
7171
+ // caller-controlled DNS target — `allow Net api.stripe.com` exit 0, measured, with the
7172
+ // sibling-free control correctly caught by AS-EFF-008. Written as the WHOLE family rather than
7173
+ // the spelling in hand (R346): every node `dns` resolver, the `dns/promises` twins (same names)
7174
+ // and the `Resolver` class methods, which share these member names.
7175
+ //
7176
+ // NOTE THE SHAPE PROBLEM THIS DOES NOT FIX. This set is an INCLUSION list, so forgetting a
7177
+ // member UNDER-reports — the opposite of `FS_USE_VERBS`/`EXEC_USE_VERBS` below, where
7178
+ // forgetting over-charges and is safe. That asymmetry is the defect class itself: java's Net
7179
+ // is sound precisely because it uses the general rule (any Net call contributing no visible
7180
+ // host leaves the surface incomplete) rather than a list. The durable repair is to INVERT this
7181
+ // into a use-verb denylist beside the other two; that is a wider change with its own
7182
+ // over-charge bill to price, so it is filed rather than smuggled in here.
7146
7183
  const NET_ESTABLISHING = new Set(["request", "get", "post", "put", "patch", "delete", "head",
7147
- "options", "connect", "createConnection", "fetch"]);
7184
+ "options", "connect", "createConnection", "fetch",
7185
+ "lookup", "lookupService", "reverse", "resolve", "resolve4", "resolve6", "resolveAny",
7186
+ "resolveCname", "resolveCaa", "resolveMx", "resolveNaptr", "resolveNs", "resolvePtr",
7187
+ "resolveSoa", "resolveSrv", "resolveTxt"]);
7148
7188
  // Fs/Exec USE-verbs whose LOCATOR was fixed earlier, not an arg of THIS call — so a missing literal
7149
7189
  // here is the legitimate split-construct/use shape, never the masking signal (the establishing-
7150
7190
  // allowlist discipline, generalized from Net to all 4 effects; sweep [11]). Fs: the fd/FileHandle