specguard-mcp 0.1.2 → 0.1.4
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 +183 -17
- package/dist/bin/specguard-mcp.js +5 -0
- package/dist/bin/specguard-mcp.js.map +1 -1
- package/dist/src/support/run-command.d.ts +42 -0
- package/dist/src/support/run-command.js +124 -8
- package/dist/src/support/run-command.js.map +1 -1
- package/dist/src/support/specguard-api.d.ts +50 -0
- package/dist/src/support/specguard-api.js +148 -8
- package/dist/src/support/specguard-api.js.map +1 -1
- package/dist/src/support/teardown.d.ts +33 -0
- package/dist/src/support/teardown.js +56 -0
- package/dist/src/support/teardown.js.map +1 -0
- package/dist/src/tools/add-repository.d.ts +55 -0
- package/dist/src/tools/add-repository.js +114 -0
- package/dist/src/tools/add-repository.js.map +1 -0
- package/dist/src/tools/args.d.ts +20 -0
- package/dist/src/tools/args.js +30 -0
- package/dist/src/tools/args.js.map +1 -1
- package/dist/src/tools/create-repository-api-key.d.ts +26 -0
- package/dist/src/tools/create-repository-api-key.js +83 -0
- package/dist/src/tools/create-repository-api-key.js.map +1 -0
- package/dist/src/tools/index.d.ts +61 -7
- package/dist/src/tools/index.js +71 -7
- package/dist/src/tools/index.js.map +1 -1
- package/dist/src/tools/list-repositories.d.ts +14 -7
- package/dist/src/tools/list-repositories.js +14 -7
- package/dist/src/tools/list-repositories.js.map +1 -1
- package/dist/src/tools/registrable-repositories.d.ts +51 -0
- package/dist/src/tools/registrable-repositories.js +92 -0
- package/dist/src/tools/registrable-repositories.js.map +1 -0
- package/dist/src/tools/remove-repository.d.ts +33 -0
- package/dist/src/tools/remove-repository.js +81 -0
- package/dist/src/tools/remove-repository.js.map +1 -0
- package/dist/src/tools/repository-overview.js +33 -8
- package/dist/src/tools/repository-overview.js.map +1 -1
- package/dist/src/tools/revoke-repository-api-key.d.ts +29 -0
- package/dist/src/tools/revoke-repository-api-key.js +85 -0
- package/dist/src/tools/revoke-repository-api-key.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,9 +11,9 @@ its prompt.
|
|
|
11
11
|
SpecGuard is built [primarily for AI coding agents](https://github.com/yatfa-ai/specguard); this
|
|
12
12
|
bridge is how an agent reaches it without scraping a web UI.
|
|
13
13
|
|
|
14
|
-
> **Status: bootstrap.**
|
|
15
|
-
>
|
|
16
|
-
> install from a checkout.
|
|
14
|
+
> **Status: bootstrap.** A small set of tools ships today, each wrapping a capability that already
|
|
15
|
+
> exists — see [The tools](#the-tools) for what is in it. The toolset **grows gradually** — see
|
|
16
|
+
> [Adding a tool](#adding-a-tool). It is not published to npm yet; install from a checkout.
|
|
17
17
|
|
|
18
18
|
## Install
|
|
19
19
|
|
|
@@ -32,9 +32,9 @@ refuses to boot and takes the tools that needed no configuration down with it.
|
|
|
32
32
|
|
|
33
33
|
| Variable | Needed by | Default | What it is |
|
|
34
34
|
| --- | --- | --- | --- |
|
|
35
|
-
| `SPECGUARD_ENDPOINT` | `get_repository_overview`, `list_repositories` | — | your SpecGuard instance's root URL, **including the scheme** — e.g. `https://specguard.example.com`, or `http://localhost:3000`. A value with no scheme is refused by name (`SPECGUARD_ENDPOINT is not a usable URL: "sg.example.com"`) rather than surfacing later as an opaque failure. `SPECGUARD_URL` is accepted as an alias, and is the name every message uses when it is the one you set. A blank value counts as unset, so leaving `SPECGUARD_ENDPOINT` empty in a templated config falls through to `SPECGUARD_URL` instead of suppressing it |
|
|
35
|
+
| `SPECGUARD_ENDPOINT` | `get_repository_overview`, `list_repositories`, `add_repository`, `registrable_repositories` | — | your SpecGuard instance's root URL, **including the scheme** — e.g. `https://specguard.example.com`, or `http://localhost:3000`. A value with no scheme is refused by name (`SPECGUARD_ENDPOINT is not a usable URL: "sg.example.com"`) rather than surfacing later as an opaque failure. `SPECGUARD_URL` is accepted as an alias, and is the name every message uses when it is the one you set. A blank value counts as unset, so leaving `SPECGUARD_ENDPOINT` empty in a templated config falls through to `SPECGUARD_URL` instead of suppressing it |
|
|
36
36
|
| `SPECGUARD_API_KEY` | `get_repository_overview` | — | an agent/CI API key (`sgk_…`) issued by that deployment |
|
|
37
|
-
| `SPECGUARD_USER_API_KEY` | `list_repositories` | — | a **user** API key (`sgu_…`), minted from that deployment's account page. A different credential from the one above, not a second place to put the same value: SpecGuard decides which of them a request may use from the token's prefix, before it reads anything, and answers `401` for the other one. Set whichever your tools need — both, if you use both |
|
|
37
|
+
| `SPECGUARD_USER_API_KEY` | `list_repositories`, `add_repository`, `registrable_repositories`, `remove_repository`, `create_repository_api_key`, `revoke_repository_api_key` | — | a **user** API key (`sgu_…`), minted from that deployment's account page. A different credential from the one above, not a second place to put the same value: SpecGuard decides which of them a request may use from the token's prefix, before it reads anything, and answers `401` for the other one. Set whichever your tools need — both, if you use both |
|
|
38
38
|
| `SPECGUARD_LINT_COMMAND` | `lint_intent_annotations` | `specguard-lint` | the command that runs the linter. Most Ruby projects need `bundle exec specguard-lint` |
|
|
39
39
|
| `SPECGUARD_TIMEOUT_MS` | HTTP tools | `30000` | how long a call to SpecGuard may take |
|
|
40
40
|
|
|
@@ -109,7 +109,7 @@ branch window rather than between the last two runs.
|
|
|
109
109
|
| `repeated_description` | open ONE repeated description and list the examples that all share it |
|
|
110
110
|
| `unstable_test` | open ONE flaky test and list its outcome run by run across the window, newest run first (needs `branch`) |
|
|
111
111
|
| `commit_sha` | anchor the answer on ONE named run instead of the repository's newest one — every run-grain block moves with it, `history` does not |
|
|
112
|
-
| `unannotated_examples` | `true` to list the individual tests
|
|
112
|
+
| `unannotated_examples` | `true` to list the individual tests carrying no `@intent` — the examples behind the annotated ratio, each labelled with what SpecGuard reads of it — and, in the same answer, which areas carry the most of them |
|
|
113
113
|
|
|
114
114
|
`branch` narrows `history` only — `latest_run` always names the repository's newest run, which on a
|
|
115
115
|
busy repo may be on another branch. That is a property of the endpoint, not of this bridge — and
|
|
@@ -210,9 +210,23 @@ description carried by two examples in one run contributes two, so `rows` is not
|
|
|
210
210
|
and its length is not the window's `run_count`.
|
|
211
211
|
|
|
212
212
|
`annotated_ratio` is the product's adoption metric and it was the one population on this endpoint
|
|
213
|
-
you could not walk down: the dashboard
|
|
213
|
+
you could not walk down: the dashboard printed *"SpecGuard cannot see the other N tests"* and could
|
|
214
214
|
not name one of them either, so an agent told to raise annotation coverage learned how far it had to
|
|
215
|
-
go and not a single test to annotate. `unannotated_examples` is that rung.
|
|
215
|
+
go and not a single test to annotate. `unannotated_examples` is that rung.
|
|
216
|
+
|
|
217
|
+
**Unannotated is not the same as unreadable, and the difference is on every response.** A test
|
|
218
|
+
called `Invoice#total sums the line items` has an entity, an action and a behavior in its own
|
|
219
|
+
description, so SpecGuard reads it whether or not anybody annotated it.
|
|
220
|
+
`latest_run.intent_readings` splits the run's examples into `authored` (an `@intent` a human wrote),
|
|
221
|
+
`derived` (read from the description) and `unreadable` (neither), with the `recorded` population
|
|
222
|
+
they were counted from — no flag to pass. **`unreadable` is the only figure on this endpoint that
|
|
223
|
+
means tests SpecGuard can say nothing about.** `total_specs - annotated_specs` is annotation debt,
|
|
224
|
+
which on a suite that has never been annotated is the whole suite and almost all of it readable;
|
|
225
|
+
never render that subtraction as blindness. A derived reading is genuinely weaker than an authored
|
|
226
|
+
one — no preconditions, a behavior written for a test runner's output rather than declared, and a
|
|
227
|
+
layer inferred from the directory — so report it as inferred and never as equivalent. And
|
|
228
|
+
`authored` never replaces `annotated_ratio`: "how much of this suite has a human-written intent" is
|
|
229
|
+
still that figure, off the run's own counters. It is the one argument
|
|
216
230
|
here that is a **flag rather than a name** — pass `true`, not a value — because it opens a
|
|
217
231
|
*population* rather than a pick: `total_specs` minus `annotated_specs` is a subtraction, and a
|
|
218
232
|
subtraction has no line to name. Which population is still yours to choose: sent alone the flag
|
|
@@ -220,8 +234,10 @@ opens the whole run, and sent **together with** `spec_file` or `spec_directory`
|
|
|
220
234
|
file, that area, or the AND of the two — those two keep opening their own blocks as well, so
|
|
221
235
|
narrowing this one is additional rather than instead. `latest_run.unannotated_examples` opens with
|
|
222
236
|
up to 100 of the unannotated examples **of whatever you asked for** (`name`, `file_path`,
|
|
223
|
-
`line_number`, `spec_file_path` each —
|
|
224
|
-
|
|
237
|
+
`line_number`, `spec_file_path`, `reading` and `derived_intent` each — six fields, and not the
|
|
238
|
+
per-example drill-ins' six: no `duration_seconds` and no `outcome`), plus that
|
|
239
|
+
same population's own `recorded_count`, `derived_count` and `unreadable_count`, the `limit` the row
|
|
240
|
+
list was cut at, and
|
|
225
241
|
`spec_file`/`spec_directory` **echoed back** as the server read them — `null` for each one you did
|
|
226
242
|
not send. Read the echo before the count: the **worklist's** `recorded_count` — and only that one,
|
|
227
243
|
because the map below deliberately does not narrow — is the figure you would reconcile against
|
|
@@ -235,13 +251,20 @@ That one ask opens **two** blocks, each in its own grain: `latest_run.unannotate
|
|
|
235
251
|
*which tests* to go and annotate, and `latest_run.unannotated_directories` for *where the debt is* —
|
|
236
252
|
the run's annotation debt rolled up by code area, which is what you pick the next `spec_directory`
|
|
237
253
|
narrowing **from**. Both come from the one flag; there is no second argument to send and no new
|
|
238
|
-
value.
|
|
239
|
-
|
|
254
|
+
value. Each worklist row carries `reading` — `"derived"` or `"unreadable"` — and `derived_intent`,
|
|
255
|
+
the `entity`/`action`/`behavior` SpecGuard got from the description or `null`; the **unreadable rows
|
|
256
|
+
come first**, so the 100-row cap cannot hide them. The map's rows carry `path`,
|
|
257
|
+
`unannotated_count`, the `recorded_count` that area was counted against, and the same three-way
|
|
258
|
+
split — `authored_count`, `derived_count` and `unreadable_count`, which sum to `recorded_count`
|
|
259
|
+
while the last two sum to `unannotated_count` (the operands, never a fraction), plus
|
|
260
|
+
`directory_count` — **every** area the run
|
|
240
261
|
touched, not every area with debt, and not `rows.size` — and its **own** `limit`, which is **10 and
|
|
241
262
|
not the worklist's 100**. Two caps under one ask, and the difference is the kind of list: 100 caps a
|
|
242
263
|
*worklist* to work through, 10 caps a *ranking* to pick from. The orders differ for the same reason —
|
|
243
|
-
the worklist is file-navigable, the map is ranked `
|
|
244
|
-
|
|
264
|
+
the worklist is file-navigable within each reading, the map is ranked `unreadable_count` descending,
|
|
265
|
+
then `unannotated_count` descending, with `path` as a tiebreak only — the areas SpecGuard cannot
|
|
266
|
+
read lead, because a ten-row ranking led by debt on an unannotated suite is a ranking by area size
|
|
267
|
+
and the dark corners never surface. A fully-annotated area is a real **row** with `unannotated_count: 0`, never an
|
|
245
268
|
omission; those rows sort last *collectively*, so on a run with more areas than the cap they are cut
|
|
246
269
|
and never seen, but on a run inside the cap they *are* listed and listed is correct. So `rows.size` is
|
|
247
270
|
not a count of areas *with* debt — read each row's `unannotated_count`. Both blocks are at run grain,
|
|
@@ -328,9 +351,152 @@ decides which table is consulted before any of them is read — so the two are n
|
|
|
328
351
|
setting one does not stand in for the other. Every message this tool produces names the variable
|
|
329
352
|
*it* reads, so a `401` here never sends you to check the key `get_repository_overview` uses.
|
|
330
353
|
|
|
331
|
-
Registering a repository
|
|
332
|
-
`
|
|
333
|
-
|
|
354
|
+
Registering a repository is `add_repository`, below — it reads the same `sgu_…` key and takes the
|
|
355
|
+
`full_name` this tool reports. Removal and the key lifecycle (`remove_repository`,
|
|
356
|
+
`create_repository_api_key`, `revoke_repository_api_key`) are below too, on the same key; a tool
|
|
357
|
+
here is a promise the agent will act on, so each waits for the capability rather than the other way
|
|
358
|
+
round.
|
|
359
|
+
|
|
360
|
+
### `add_repository`
|
|
361
|
+
|
|
362
|
+
Registers a GitHub repository with SpecGuard for the person behind `SPECGUARD_USER_API_KEY`, and
|
|
363
|
+
returns the repository together with its **first CI API key** — the `sgk_…` key that repository's CI
|
|
364
|
+
will use to ingest runs, minted in the same call so a fresh registration is usable without a second
|
|
365
|
+
trip through the browser.
|
|
366
|
+
|
|
367
|
+
| argument | |
|
|
368
|
+
| --- | --- |
|
|
369
|
+
| `full_name` | the repository to register, as `org/repo` (for example `acme/billing`) — the same handle `list_repositories` reports. Not a URL, not a bare repository name |
|
|
370
|
+
|
|
371
|
+
The body comes back as SpecGuard serves it: a `repository` block (`id`, `full_name`, `name`,
|
|
372
|
+
`registered_at` — deliberately the same four fields `get_repository_overview` serves in its own
|
|
373
|
+
`repository` block) and an `api_key` block (`name`, `token`, `hint`, `created_at`).
|
|
374
|
+
|
|
375
|
+
> ⚠️ **`api_key.token` is shown once and never again.** Nothing stores it and no endpoint can
|
|
376
|
+
> re-serve it. Capture it from this response — an agent should hand it straight to the person it is
|
|
377
|
+
> working for. A key that is lost is replaced from SpecGuard's API-keys page in a browser, not from
|
|
378
|
+
> here.
|
|
379
|
+
|
|
380
|
+
> ⚠️ **This tool is not idempotent, and it writes.** If the call exceeds `SPECGUARD_TIMEOUT_MS` the
|
|
381
|
+
> bridge gives up, but the registration may still have succeeded on the server — taking its one-time
|
|
382
|
+
> token into a response nobody received. The retry is then refused with `has already been taken`,
|
|
383
|
+
> which is the honest answer rather than a bug. Do not retry a timeout blindly; check
|
|
384
|
+
> `list_repositories`, and recover the key in the browser.
|
|
385
|
+
|
|
386
|
+
**It needs a current record of your GitHub permissions, and only a browser creates one.** SpecGuard
|
|
387
|
+
decides whether you may register a repository from a stored grant, and fails closed when that grant
|
|
388
|
+
is missing or stale — which is every person who has not signed in and connected GitHub recently. That
|
|
389
|
+
refusal arrives as SpecGuard's own sentence, verbatim, naming the fix: *sign in to SpecGuard in a
|
|
390
|
+
browser and reconnect GitHub, then try again*. No argument to this tool substitutes for it. The same
|
|
391
|
+
path carries the other refusals — a repository the SpecGuard GitHub App is not installed on, one you
|
|
392
|
+
do not administer, one already registered.
|
|
393
|
+
|
|
394
|
+
**The `org/repo` format is not re-checked here.** This bridge verifies only that you passed a
|
|
395
|
+
non-blank string; SpecGuard validates the name and refuses an unusable one in its own words. A second
|
|
396
|
+
format rule on this side would be free to drift from the one that actually decides, and would surface
|
|
397
|
+
as this bridge rejecting a name the platform would have accepted.
|
|
398
|
+
|
|
399
|
+
It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and a
|
|
400
|
+
different one from the `sgk_…` key `get_repository_overview` uses.
|
|
401
|
+
|
|
402
|
+
### `registrable_repositories`
|
|
403
|
+
|
|
404
|
+
Lists the GitHub repositories the person behind `SPECGUARD_USER_API_KEY` **could** register with
|
|
405
|
+
SpecGuard — the set the registration gate would consult, read out loud in advance, so an agent can
|
|
406
|
+
pick a `full_name` for `add_repository` from a real answer rather than by guessing. `list_repositories`
|
|
407
|
+
reports what *is* registered; this reports what could be, and the two answer different questions.
|
|
408
|
+
|
|
409
|
+
The body comes back as SpecGuard serves it: `{"repositories": […]}` with each entry carrying
|
|
410
|
+
`full_name` and `registered`, ordered by `full_name` ascending, plus a `grant` block (`captured_at`,
|
|
411
|
+
`expires_at`, `stale`) describing the stored record of this person's GitHub permissions.
|
|
412
|
+
|
|
413
|
+
**`registered` is asked globally, not just of your own repositories.** An entry marked
|
|
414
|
+
`registered: true` was registered by *somebody* — possibly someone else — and a POST naming it will
|
|
415
|
+
be refused with `has already been taken`. That is exactly why entries are *marked* rather than
|
|
416
|
+
excluded: a reading scoped to what you can open would send you at a name nobody can register.
|
|
417
|
+
|
|
418
|
+
**A name appearing here is not a promise the write will succeed.** This is the set the gate would
|
|
419
|
+
consult at the moment of the read; the repository may be registered by someone else between this
|
|
420
|
+
call and your POST.
|
|
421
|
+
|
|
422
|
+
**A missing or stale grant is not an error — it is the modal first answer.** SpecGuard fails closed
|
|
423
|
+
when it has no current record of your GitHub permissions, which is every person who has not opened
|
|
424
|
+
SpecGuard in a browser recently. The call then answers `403` with SpecGuard's own sentence naming
|
|
425
|
+
the fix: *sign in to SpecGuard in a browser and reconnect GitHub, then try again*. On that refusal
|
|
426
|
+
the body still carries `grant`, and it distinguishes the two cases: `grant: null` means there never
|
|
427
|
+
was one (first-time setup), a populated grant with `stale: true` means an existing connection lapsed
|
|
428
|
+
— same remedy, very different urgency. Read it before telling the person what to do.
|
|
429
|
+
|
|
430
|
+
**This tool takes no arguments** — the credential is the whole of the scope. The endpoint takes no
|
|
431
|
+
parameters; which repositories are in the answer is decided by SpecGuard from the person the key
|
|
432
|
+
speaks for.
|
|
433
|
+
|
|
434
|
+
It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
|
|
435
|
+
`add_repository` and a different one from the `sgk_…` key `get_repository_overview` uses.
|
|
436
|
+
|
|
437
|
+
### `remove_repository`
|
|
438
|
+
|
|
439
|
+
Removes a repository from SpecGuard — and with it **every key, run and intent on it**. This is the
|
|
440
|
+
destructive gesture in this toolset: irreversible, no undo, and a `204` means the repository and its
|
|
441
|
+
history are gone short of re-registering from scratch. Confirm with the user before calling.
|
|
442
|
+
|
|
443
|
+
| argument | |
|
|
444
|
+
| --- | --- |
|
|
445
|
+
| `repository_id` | the repository to remove — its numeric `id`, as `add_repository` returns and `list_repositories` reports, not the `org/repo` handle |
|
|
446
|
+
|
|
447
|
+
Authorization is the `repo.delete` capability at **either surface** — an owner, or a member granted
|
|
448
|
+
`repo.delete`, may remove the repository. A member without it is refused `403` with SpecGuard's own
|
|
449
|
+
sentence, verbatim. The repository's CI keys stop authenticating the moment it succeeds.
|
|
450
|
+
|
|
451
|
+
It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
|
|
452
|
+
`add_repository` and a different one from the `sgk_…` key `get_repository_overview` uses.
|
|
453
|
+
|
|
454
|
+
### `create_repository_api_key`
|
|
455
|
+
|
|
456
|
+
Mints a new CI API key (`sgk_…`) for a SpecGuard repository and returns it alongside the
|
|
457
|
+
repository's existing keys. Minting does not disturb existing keys — each key on a repository
|
|
458
|
+
authenticates independently until revoked.
|
|
459
|
+
|
|
460
|
+
| argument | |
|
|
461
|
+
| --- | --- |
|
|
462
|
+
| `repository_id` | the repository to mint the key for — its numeric `id`, as `add_repository` returns and `list_repositories` reports, not the `org/repo` handle |
|
|
463
|
+
| `name` | an optional label for the key; omit it to let SpecGuard use its default name |
|
|
464
|
+
|
|
465
|
+
The body comes back as SpecGuard serves it: an `api_key` block (`name`, `token`, `hint`,
|
|
466
|
+
`created_at`) — the same shape `add_repository` serves.
|
|
467
|
+
|
|
468
|
+
> ⚠️ **`api_key.token` is shown once and never again.** Nothing stores it and no endpoint can
|
|
469
|
+
> re-serve it. Hand it to the person you are working for in your reply. If it is dropped, the
|
|
470
|
+
> recovery is minting another key with this same tool — the platform has no regenerate — then
|
|
471
|
+
> revoking the orphaned one with `revoke_repository_api_key`.
|
|
472
|
+
|
|
473
|
+
Authorization is the `keys_manage` capability; a member without it is refused `403` with SpecGuard's
|
|
474
|
+
own sentence, verbatim.
|
|
475
|
+
|
|
476
|
+
It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
|
|
477
|
+
`add_repository` and a different one from the `sgk_…` key `get_repository_overview` uses.
|
|
478
|
+
|
|
479
|
+
### `revoke_repository_api_key`
|
|
480
|
+
|
|
481
|
+
Revokes one CI API key on a SpecGuard repository. The key stops authenticating immediately; every
|
|
482
|
+
**other** key on the repository keeps working, so CI keeps ingesting if it holds a surviving key.
|
|
483
|
+
|
|
484
|
+
| argument | |
|
|
485
|
+
| --- | --- |
|
|
486
|
+
| `repository_id` | the repository the key belongs to — its numeric `id`, as `add_repository` returns and `list_repositories` reports, not the `org/repo` handle |
|
|
487
|
+
| `key_id` | the id of the key to revoke, as served in the `api_key` block of `add_repository` or `create_repository_api_key` |
|
|
488
|
+
|
|
489
|
+
The `key_id` is scoped to `repository_id`: a key id belonging to a different repository is refused
|
|
490
|
+
`404`, never a cross-repository delete. Authorization is the `keys_manage` capability; a member
|
|
491
|
+
without it is refused `403` with SpecGuard's own sentence, verbatim.
|
|
492
|
+
|
|
493
|
+
**Key rotation is mint-then-revoke, in that order.** The platform has no regenerate, so mint a
|
|
494
|
+
replacement with `create_repository_api_key` and deploy it BEFORE revoking the old one — revoke
|
|
495
|
+
first and the repository's CI is locked out until a human mints a new key in a browser. A `204`
|
|
496
|
+
means the key is revoked.
|
|
497
|
+
|
|
498
|
+
It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and
|
|
499
|
+
`add_repository` and a different one from the `sgk_…` key `get_repository_overview` uses.
|
|
334
500
|
|
|
335
501
|
## How it works
|
|
336
502
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
import { createServer } from "../src/server.js";
|
|
4
|
+
import { installTeardown } from "../src/support/teardown.js";
|
|
4
5
|
/**
|
|
5
6
|
* The stdio entrypoint — the only place a transport is named.
|
|
6
7
|
*
|
|
@@ -21,6 +22,10 @@ import { createServer } from "../src/server.js";
|
|
|
21
22
|
* it.
|
|
22
23
|
*/
|
|
23
24
|
async function main() {
|
|
25
|
+
// Installed BEFORE the transport is connected, so there is no window in which
|
|
26
|
+
// the server is accepting tool calls — and therefore spawning runs — with no
|
|
27
|
+
// handler to clean them up.
|
|
28
|
+
installTeardown();
|
|
24
29
|
const server = createServer();
|
|
25
30
|
await server.connect(new StdioServerTransport());
|
|
26
31
|
process.stderr.write("specguard-mcp: ready on stdio\n");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"specguard-mcp.js","sourceRoot":"","sources":["../../bin/specguard-mcp.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"specguard-mcp.js","sourceRoot":"","sources":["../../bin/specguard-mcp.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAE7D;;;;;;;;;;;;;;;;;;GAkBG;AACH,KAAK,UAAU,IAAI;IACjB,8EAA8E;IAC9E,6EAA6E;IAC7E,4BAA4B;IAC5B,eAAe,EAAE,CAAC;IAElB,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;IAC9B,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,oBAAoB,EAAE,CAAC,CAAC;IACjD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,iCAAiC,CAAC,CAAC;AAC1D,CAAC;AAED,4EAA4E;AAC5E,gFAAgF;AAChF,0EAA0E;AAC1E,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC9B,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,yBAAyB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CACnG,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -68,6 +68,48 @@ export declare const DEFAULT_COMMAND_TIMEOUT_MS = 120000;
|
|
|
68
68
|
* descriptor costs a truncated tail instead of a call that never returns.
|
|
69
69
|
*/
|
|
70
70
|
export declare const EXIT_CLOSE_GRACE_MS = 1000;
|
|
71
|
+
/**
|
|
72
|
+
* The pids of every run currently spawned and not yet reaped.
|
|
73
|
+
*
|
|
74
|
+
* Exported for the teardown handler's diagnostics. A PROJECTION, not the set:
|
|
75
|
+
* it drops entries whose spawn never produced a process, because a `undefined`
|
|
76
|
+
* pid is not something a diagnostic can name or a caller can signal.
|
|
77
|
+
*
|
|
78
|
+
* That filter makes it the WRONG observer for asserting the set does not leak —
|
|
79
|
+
* see `outstandingRunCount`.
|
|
80
|
+
*/
|
|
81
|
+
export declare function outstandingRunPids(): readonly number[];
|
|
82
|
+
/**
|
|
83
|
+
* How many runs are registered, counting those whose spawn produced no pid.
|
|
84
|
+
*
|
|
85
|
+
* The unfiltered companion to `outstandingRunPids`, and the one a leak test must
|
|
86
|
+
* use. The distinction is not pedantic: the spawn-failure path registers a child
|
|
87
|
+
* whose `pid` is `undefined`, so it is invisible to `outstandingRunPids` BY
|
|
88
|
+
* EXACTLY THE PROPERTY THAT MAKES IT A LEAK. A test asserting that path through
|
|
89
|
+
* the pid projection holds whether or not the deregistration happens, and would
|
|
90
|
+
* stay green if a refactor dropped it.
|
|
91
|
+
*
|
|
92
|
+
* The leak is memory-only — `killRun` returns `false` for an undefined pid, so a
|
|
93
|
+
* stranded entry is skipped by the drain rather than mis-signalled — but it is
|
|
94
|
+
* one entry per failed spawn for the life of the server, and it is only ever
|
|
95
|
+
* observable from outside. Hence a reader of the set itself.
|
|
96
|
+
*/
|
|
97
|
+
export declare function outstandingRunCount(): number;
|
|
98
|
+
/**
|
|
99
|
+
* Kills every run still in flight, and answers how many it signalled.
|
|
100
|
+
*
|
|
101
|
+
* The teardown path. SYNCHRONOUS AND UNBOUNDED BY NOTHING — it sends signals and
|
|
102
|
+
* returns, it never waits for a child to die. That is deliberate: this runs from
|
|
103
|
+
* a SIGINT/SIGTERM handler, where anything that waits is something that can hang
|
|
104
|
+
* the shutdown it was supposed to perform. SIGKILL is not refusable, so there is
|
|
105
|
+
* no acknowledgement worth waiting for.
|
|
106
|
+
*
|
|
107
|
+
* It reuses `killRun` rather than re-deriving the kill, so the group-vs-child
|
|
108
|
+
* fallback and the spawn-failure guard have exactly one implementation. The set
|
|
109
|
+
* is snapshotted before iterating because `killRun` can drive an `exit` that
|
|
110
|
+
* mutates it.
|
|
111
|
+
*/
|
|
112
|
+
export declare function killOutstandingRuns(): number;
|
|
71
113
|
/**
|
|
72
114
|
* Runs a program with an argument LIST, never through a shell.
|
|
73
115
|
*
|
|
@@ -15,6 +15,95 @@ export const DEFAULT_COMMAND_TIMEOUT_MS = 120_000;
|
|
|
15
15
|
* descriptor costs a truncated tail instead of a call that never returns.
|
|
16
16
|
*/
|
|
17
17
|
export const EXIT_CLOSE_GRACE_MS = 1_000;
|
|
18
|
+
/**
|
|
19
|
+
* The runs that are spawned and not yet reaped.
|
|
20
|
+
*
|
|
21
|
+
* This exists because `detached: true` below buys the reach of the timeout kill
|
|
22
|
+
* and PAYS for it: a detached child is in a new session, outside this server's
|
|
23
|
+
* controlling terminal, so a signal aimed at OUR group — an interactive Ctrl-C,
|
|
24
|
+
* a supervisor's `kill -- -PGID` — no longer reaches a lint run in flight. The
|
|
25
|
+
* run is then orphaned, and orphaned WITHOUT A DEADLINE: the 120s ceiling is a
|
|
26
|
+
* parent-side `setTimeout`, so killing the parent destroys the only thing that
|
|
27
|
+
* was going to stop it. On a 20k-example suite that is a full lint's worth of
|
|
28
|
+
* CPU held by a process attached to nothing. `killOutstandingRuns` is the
|
|
29
|
+
* teardown path that closes it, and this set is what tells it whom to signal.
|
|
30
|
+
*
|
|
31
|
+
* MEMBERSHIP MEANS "NOT YET REAPED", and that is load-bearing rather than
|
|
32
|
+
* descriptive: `killRun` signals a raw negated pid, which has no liveness check
|
|
33
|
+
* of its own, so a stale entry is a SIGKILL aimed at whatever recycled that pid.
|
|
34
|
+
* Entries are therefore removed on the child's own `exit` — the reap — and NOT
|
|
35
|
+
* at settle, which on the grace-backstop path happens up to EXIT_CLOSE_GRACE_MS
|
|
36
|
+
* later. Deleting at settle would leave exactly the window in which a drain
|
|
37
|
+
* would signal a freed pid.
|
|
38
|
+
*
|
|
39
|
+
* The accepted consequence is the one this file already takes at `killRun`: a
|
|
40
|
+
* straggler outliving an already-exited child is not killed at teardown. That
|
|
41
|
+
* trade is deliberate and is not widened here.
|
|
42
|
+
*/
|
|
43
|
+
const liveChildren = new Set();
|
|
44
|
+
/**
|
|
45
|
+
* The pids of every run currently spawned and not yet reaped.
|
|
46
|
+
*
|
|
47
|
+
* Exported for the teardown handler's diagnostics. A PROJECTION, not the set:
|
|
48
|
+
* it drops entries whose spawn never produced a process, because a `undefined`
|
|
49
|
+
* pid is not something a diagnostic can name or a caller can signal.
|
|
50
|
+
*
|
|
51
|
+
* That filter makes it the WRONG observer for asserting the set does not leak —
|
|
52
|
+
* see `outstandingRunCount`.
|
|
53
|
+
*/
|
|
54
|
+
export function outstandingRunPids() {
|
|
55
|
+
const pids = [];
|
|
56
|
+
for (const child of liveChildren) {
|
|
57
|
+
if (child.pid !== undefined)
|
|
58
|
+
pids.push(child.pid);
|
|
59
|
+
}
|
|
60
|
+
return pids;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* How many runs are registered, counting those whose spawn produced no pid.
|
|
64
|
+
*
|
|
65
|
+
* The unfiltered companion to `outstandingRunPids`, and the one a leak test must
|
|
66
|
+
* use. The distinction is not pedantic: the spawn-failure path registers a child
|
|
67
|
+
* whose `pid` is `undefined`, so it is invisible to `outstandingRunPids` BY
|
|
68
|
+
* EXACTLY THE PROPERTY THAT MAKES IT A LEAK. A test asserting that path through
|
|
69
|
+
* the pid projection holds whether or not the deregistration happens, and would
|
|
70
|
+
* stay green if a refactor dropped it.
|
|
71
|
+
*
|
|
72
|
+
* The leak is memory-only — `killRun` returns `false` for an undefined pid, so a
|
|
73
|
+
* stranded entry is skipped by the drain rather than mis-signalled — but it is
|
|
74
|
+
* one entry per failed spawn for the life of the server, and it is only ever
|
|
75
|
+
* observable from outside. Hence a reader of the set itself.
|
|
76
|
+
*/
|
|
77
|
+
export function outstandingRunCount() {
|
|
78
|
+
return liveChildren.size;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Kills every run still in flight, and answers how many it signalled.
|
|
82
|
+
*
|
|
83
|
+
* The teardown path. SYNCHRONOUS AND UNBOUNDED BY NOTHING — it sends signals and
|
|
84
|
+
* returns, it never waits for a child to die. That is deliberate: this runs from
|
|
85
|
+
* a SIGINT/SIGTERM handler, where anything that waits is something that can hang
|
|
86
|
+
* the shutdown it was supposed to perform. SIGKILL is not refusable, so there is
|
|
87
|
+
* no acknowledgement worth waiting for.
|
|
88
|
+
*
|
|
89
|
+
* It reuses `killRun` rather than re-deriving the kill, so the group-vs-child
|
|
90
|
+
* fallback and the spawn-failure guard have exactly one implementation. The set
|
|
91
|
+
* is snapshotted before iterating because `killRun` can drive an `exit` that
|
|
92
|
+
* mutates it.
|
|
93
|
+
*/
|
|
94
|
+
export function killOutstandingRuns() {
|
|
95
|
+
const children = [...liveChildren];
|
|
96
|
+
let signalled = 0;
|
|
97
|
+
for (const child of children) {
|
|
98
|
+
// Removed FIRST, so the entry is gone even if the kill throws. A registry
|
|
99
|
+
// that kept a child it had already tried to kill would hand a second drain
|
|
100
|
+
// the same stale pid.
|
|
101
|
+
liveChildren.delete(child);
|
|
102
|
+
if (killRun(child))
|
|
103
|
+
signalled += 1;
|
|
104
|
+
}
|
|
105
|
+
return signalled;
|
|
106
|
+
}
|
|
18
107
|
/**
|
|
19
108
|
* Runs a program with an argument LIST, never through a shell.
|
|
20
109
|
*
|
|
@@ -128,6 +217,11 @@ export const runCommand = (argv, options = {}) => {
|
|
|
128
217
|
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
129
218
|
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
130
219
|
child.on("error", (error) => {
|
|
220
|
+
// The spawn-failure path: there is no process, and `child.pid` is
|
|
221
|
+
// `undefined`, so this entry can never be a legitimate kill target. Dropped
|
|
222
|
+
// here because `exit` does not always follow an `error` — leaving it would
|
|
223
|
+
// strand an unkillable entry in the registry for the life of the server.
|
|
224
|
+
liveChildren.delete(child);
|
|
131
225
|
finish(() => reject(new CommandError(describeSpawnFailure(program, error, options))));
|
|
132
226
|
});
|
|
133
227
|
/**
|
|
@@ -153,6 +247,20 @@ export const runCommand = (argv, options = {}) => {
|
|
|
153
247
|
// on we hold a real `code`/`signal`, so whatever the timer may still find
|
|
154
248
|
// alive in the process group, this is not a run that produced no verdict.
|
|
155
249
|
exited = true;
|
|
250
|
+
// Deregistered HERE — at the reap, alongside `exited`, and deliberately not
|
|
251
|
+
// at the settle below. The two are not the same instant: on the grace
|
|
252
|
+
// backstop `exit` fires and `settleWith` follows up to EXIT_CLOSE_GRACE_MS
|
|
253
|
+
// later, so a registry keyed on settle would hold a child whose pid the
|
|
254
|
+
// kernel has already freed, and a teardown drain landing in that window
|
|
255
|
+
// would fire `process.kill(-pid)` at whatever now owns that number. That
|
|
256
|
+
// is the unrecoverable, aimed-at-a-stranger hazard `killRun` documents and
|
|
257
|
+
// refuses to pay; membership must mean "not yet reaped" so its precondition
|
|
258
|
+
// holds by construction.
|
|
259
|
+
//
|
|
260
|
+
// Placed BEFORE the `settled` early-return for the same reason: the
|
|
261
|
+
// already-settled path is a reap too, and returning first would leak the
|
|
262
|
+
// entry.
|
|
263
|
+
liveChildren.delete(child);
|
|
156
264
|
if (settled)
|
|
157
265
|
return;
|
|
158
266
|
graceTimer = setTimeout(() => {
|
|
@@ -224,7 +332,7 @@ function killRun(child) {
|
|
|
224
332
|
* promise losing the child's type.
|
|
225
333
|
*/
|
|
226
334
|
function spawnChild(program, args, options) {
|
|
227
|
-
|
|
335
|
+
const child = spawn(program, [...args], {
|
|
228
336
|
cwd: options.cwd,
|
|
229
337
|
stdio: ["ignore", "pipe", "pipe"],
|
|
230
338
|
// Explicitly off. Stated rather than defaulted, because this is the line
|
|
@@ -240,15 +348,23 @@ function spawnChild(program, args, options) {
|
|
|
240
348
|
// that detaching buys the reach of the kill and PAYS for it here: a new
|
|
241
349
|
// group is also a new session, outside this server's controlling terminal,
|
|
242
350
|
// so a signal aimed at OUR group — an interactive Ctrl-C, a supervisor's
|
|
243
|
-
// `kill -- -PGID` — no longer reaches a lint run in flight.
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
351
|
+
// `kill -- -PGID` — no longer reaches a lint run in flight. Such a run would
|
|
352
|
+
// be orphaned where it would previously have died alongside us, and orphaned
|
|
353
|
+
// without a deadline, since the ceiling above is a parent-side timer.
|
|
354
|
+
//
|
|
355
|
+
// That is the debt this line used to carry, and it is now PAID rather than
|
|
356
|
+
// merely disclosed: the run is registered in `liveChildren` below, and
|
|
357
|
+
// `bin/specguard-mcp.ts` installs SIGINT/SIGTERM handlers that call
|
|
358
|
+
// `killOutstandingRuns` before exiting. The trade the kill's reach was
|
|
359
|
+
// bought with is closed; do not remove either half without restoring the
|
|
360
|
+
// other.
|
|
250
361
|
detached: true,
|
|
251
362
|
});
|
|
363
|
+
// Registered at the single spawn call site, so a run cannot enter the world
|
|
364
|
+
// unregistered. Removed again on the child's own `exit` — see `liveChildren`
|
|
365
|
+
// for why the reap, and not the settle, is the moment that matters.
|
|
366
|
+
liveChildren.add(child);
|
|
367
|
+
return child;
|
|
252
368
|
}
|
|
253
369
|
/**
|
|
254
370
|
* Which thing failed to run — asked rather than assumed.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-command.js","sourceRoot":"","sources":["../../../src/support/run-command.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAgE5C,iFAAiF;AACjF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AAEhD,6DAA6D;AAC7D,MAAM,CAAC,MAAM,0BAA0B,GAAG,OAAO,CAAC;AAElD;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"run-command.js","sourceRoot":"","sources":["../../../src/support/run-command.ts"],"names":[],"mappings":"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"}
|
|
@@ -8,6 +8,56 @@ import { requireApiConfig, requireUserApiConfig, type ApiConfig } from "../confi
|
|
|
8
8
|
* second place for the permission model to be got wrong.
|
|
9
9
|
*/
|
|
10
10
|
export declare function getJson(api: ApiConfig, path: string, query: Record<string, string | undefined>, fetchImpl: typeof globalThis.fetch): Promise<unknown>;
|
|
11
|
+
/**
|
|
12
|
+
* `POST` with a JSON body — the write half of the transport, and deliberately
|
|
13
|
+
* the SAME function underneath.
|
|
14
|
+
*
|
|
15
|
+
* It shares `fetchWithTimeout` rather than standing beside it. The one-total-
|
|
16
|
+
* budget deadline, the explicit race, the `unref`'d timer, the abort and the
|
|
17
|
+
* "reached and stopped" vs "could not reach" split are the expensive part of
|
|
18
|
+
* this module and every argument for them is written above them — none of it is
|
|
19
|
+
* about the verb. A second transport re-deriving them is how the two come to
|
|
20
|
+
* disagree about what `SPECGUARD_TIMEOUT_MS` bounds, and the write path is the
|
|
21
|
+
* one where a call that never returns costs the most: the agent has already
|
|
22
|
+
* committed to a registration by the time it hangs.
|
|
23
|
+
*
|
|
24
|
+
* The body is serialized HERE rather than taken as a string, so no caller can
|
|
25
|
+
* send a body whose `Content-Type` says JSON and whose bytes are not.
|
|
26
|
+
*/
|
|
27
|
+
export declare function postJson(api: ApiConfig, path: string, body: Record<string, unknown>, fetchImpl: typeof globalThis.fetch): Promise<unknown>;
|
|
28
|
+
/**
|
|
29
|
+
* `DELETE` — the destructive half of the transport, and deliberately the SAME
|
|
30
|
+
* function underneath `postJson` rather than beside it, for the reason
|
|
31
|
+
* `postJson`'s header states: everything expensive about this module is about
|
|
32
|
+
* the deadline, not the verb.
|
|
33
|
+
*
|
|
34
|
+
* Returns the RAW BODY TEXT rather than a parsed value, because the endpoints
|
|
35
|
+
* this serves answer `204` with NO body at all — the one response in the `sgu_`
|
|
36
|
+
* surface that is deliberately not JSON. `requestJson` JSON-parses every 2xx it
|
|
37
|
+
* sees, so routing a `204` through it would turn a successful delete into
|
|
38
|
+
* "answered 204 but the body was not JSON" — the trap this verb specifically
|
|
39
|
+
* introduces, and the reason the DELETE path has its own success handling
|
|
40
|
+
* instead of sharing `requestJson`'s. The status check and the
|
|
41
|
+
* "reached and refused" hand-off to `describeFailure` are still shared
|
|
42
|
+
* verbatim: only what happens to a SUCCESS body differs.
|
|
43
|
+
*/
|
|
44
|
+
export declare function deleteJson(api: ApiConfig, path: string, fetchImpl: typeof globalThis.fetch): Promise<string>;
|
|
45
|
+
/**
|
|
46
|
+
* `postJson`, narrowed exactly as `getJsonObject` narrows `getJson`.
|
|
47
|
+
*
|
|
48
|
+
* The write path needs the same guard for the same reason, and the reason is not
|
|
49
|
+
* about reading: `ToolResult.structured` is a `Record<string, unknown>`, so a
|
|
50
|
+
* body that is an array or a bare scalar is not something a tool can pass
|
|
51
|
+
* through whichever verb fetched it. Shipping only the raw `postJson` would
|
|
52
|
+
* leave the first write tool to re-type the three-clause check and its sentence
|
|
53
|
+
* — which is precisely the duplication `getJsonObject`'s header says no tool
|
|
54
|
+
* should have to repeat.
|
|
55
|
+
*
|
|
56
|
+
* The pair is mirrored rather than collapsed for the reason the read pair is:
|
|
57
|
+
* `postJson` stays exported un-narrowed for an endpoint that legitimately
|
|
58
|
+
* answers with an array.
|
|
59
|
+
*/
|
|
60
|
+
export declare function postJsonObject(api: ApiConfig, path: string, body: Record<string, unknown>, fetchImpl: typeof globalThis.fetch): Promise<Record<string, unknown>>;
|
|
11
61
|
/**
|
|
12
62
|
* `getJson`, narrowed to the object every tool here actually asks it for.
|
|
13
63
|
*
|