specguard-mcp 0.1.1 → 0.1.3
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 +129 -22
- package/dist/bin/specguard-mcp.js +5 -0
- package/dist/bin/specguard-mcp.js.map +1 -1
- package/dist/src/config.d.ts +104 -14
- package/dist/src/config.js +78 -7
- package/dist/src/config.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.js +6 -1
- package/dist/src/index.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 +53 -2
- package/dist/src/support/specguard-api.js +153 -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/index.d.ts +33 -0
- package/dist/src/tools/index.js +41 -1
- package/dist/src/tools/index.js.map +1 -1
- package/dist/src/tools/list-repositories.d.ts +75 -0
- package/dist/src/tools/list-repositories.js +112 -0
- package/dist/src/tools/list-repositories.js.map +1 -0
- package/dist/src/tools/repository-overview.js +35 -15
- package/dist/src/tools/repository-overview.js.map +1 -1
- 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,14 +32,17 @@ 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` | — | 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` | — | 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
38
|
| `SPECGUARD_LINT_COMMAND` | `lint_intent_annotations` | `specguard-lint` | the command that runs the linter. Most Ruby projects need `bundle exec specguard-lint` |
|
|
38
39
|
| `SPECGUARD_TIMEOUT_MS` | HTTP tools | `30000` | how long a call to SpecGuard may take |
|
|
39
40
|
|
|
40
41
|
`SPECGUARD_ENDPOINT` and `SPECGUARD_API_KEY` are the same variables
|
|
41
42
|
[`specguard-rspec`](https://github.com/yatfa-ai/specguard-rspec) uses to ship a run, so a repository
|
|
42
|
-
that already posts telemetry to SpecGuard already has them.
|
|
43
|
+
that already posts telemetry to SpecGuard already has them. `SPECGUARD_USER_API_KEY` is **not** one
|
|
44
|
+
of them — the gem has no notion of a user key — so that one is minted and set here for the first
|
|
45
|
+
time.
|
|
43
46
|
|
|
44
47
|
Register it with your MCP client — for Claude Code:
|
|
45
48
|
|
|
@@ -52,6 +55,7 @@ Register it with your MCP client — for Claude Code:
|
|
|
52
55
|
"env": {
|
|
53
56
|
"SPECGUARD_ENDPOINT": "https://specguard.example.com",
|
|
54
57
|
"SPECGUARD_API_KEY": "sgk_…",
|
|
58
|
+
"SPECGUARD_USER_API_KEY": "sgu_…",
|
|
55
59
|
"SPECGUARD_LINT_COMMAND": "bundle exec specguard-lint"
|
|
56
60
|
}
|
|
57
61
|
}
|
|
@@ -105,7 +109,7 @@ branch window rather than between the last two runs.
|
|
|
105
109
|
| `repeated_description` | open ONE repeated description and list the examples that all share it |
|
|
106
110
|
| `unstable_test` | open ONE flaky test and list its outcome run by run across the window, newest run first (needs `branch`) |
|
|
107
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 |
|
|
108
|
-
| `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 |
|
|
109
113
|
|
|
110
114
|
`branch` narrows `history` only — `latest_run` always names the repository's newest run, which on a
|
|
111
115
|
busy repo may be on another branch. That is a property of the endpoint, not of this bridge — and
|
|
@@ -206,9 +210,23 @@ description carried by two examples in one run contributes two, so `rows` is not
|
|
|
206
210
|
and its length is not the window's `run_count`.
|
|
207
211
|
|
|
208
212
|
`annotated_ratio` is the product's adoption metric and it was the one population on this endpoint
|
|
209
|
-
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
|
|
210
214
|
not name one of them either, so an agent told to raise annotation coverage learned how far it had to
|
|
211
|
-
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
|
|
212
230
|
here that is a **flag rather than a name** — pass `true`, not a value — because it opens a
|
|
213
231
|
*population* rather than a pick: `total_specs` minus `annotated_specs` is a subtraction, and a
|
|
214
232
|
subtraction has no line to name. Which population is still yours to choose: sent alone the flag
|
|
@@ -216,8 +234,10 @@ opens the whole run, and sent **together with** `spec_file` or `spec_directory`
|
|
|
216
234
|
file, that area, or the AND of the two — those two keep opening their own blocks as well, so
|
|
217
235
|
narrowing this one is additional rather than instead. `latest_run.unannotated_examples` opens with
|
|
218
236
|
up to 100 of the unannotated examples **of whatever you asked for** (`name`, `file_path`,
|
|
219
|
-
`line_number`, `spec_file_path` each —
|
|
220
|
-
|
|
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
|
|
221
241
|
`spec_file`/`spec_directory` **echoed back** as the server read them — `null` for each one you did
|
|
222
242
|
not send. Read the echo before the count: the **worklist's** `recorded_count` — and only that one,
|
|
223
243
|
because the map below deliberately does not narrow — is the figure you would reconcile against
|
|
@@ -231,13 +251,20 @@ That one ask opens **two** blocks, each in its own grain: `latest_run.unannotate
|
|
|
231
251
|
*which tests* to go and annotate, and `latest_run.unannotated_directories` for *where the debt is* —
|
|
232
252
|
the run's annotation debt rolled up by code area, which is what you pick the next `spec_directory`
|
|
233
253
|
narrowing **from**. Both come from the one flag; there is no second argument to send and no new
|
|
234
|
-
value.
|
|
235
|
-
|
|
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
|
|
236
261
|
touched, not every area with debt, and not `rows.size` — and its **own** `limit`, which is **10 and
|
|
237
262
|
not the worklist's 100**. Two caps under one ask, and the difference is the kind of list: 100 caps a
|
|
238
263
|
*worklist* to work through, 10 caps a *ranking* to pick from. The orders differ for the same reason —
|
|
239
|
-
the worklist is file-navigable, the map is ranked `
|
|
240
|
-
|
|
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
|
|
241
268
|
omission; those rows sort last *collectively*, so on a run with more areas than the cap they are cut
|
|
242
269
|
and never seen, but on a run inside the cap they *are* listed and listed is correct. So `rows.size` is
|
|
243
270
|
not a count of areas *with* debt — read each row's `unannotated_count`. Both blocks are at run grain,
|
|
@@ -297,6 +324,80 @@ a finding and not a disclosure someone forgot.
|
|
|
297
324
|
Figures are `null` where CI did not report them. A `null` means *not measured*; it is never a zero,
|
|
298
325
|
because a zero would read as a measurement that was taken.
|
|
299
326
|
|
|
327
|
+
### `list_repositories`
|
|
328
|
+
|
|
329
|
+
Lists the SpecGuard repositories the person behind `SPECGUARD_USER_API_KEY` may open — *what can I
|
|
330
|
+
ask about*, which is the one question no other tool here can answer. `get_repository_overview` takes
|
|
331
|
+
no repository because its `sgk_…` key **is** the repository, so without this an agent can only report
|
|
332
|
+
on a repository somebody already named for it.
|
|
333
|
+
|
|
334
|
+
**This tool takes no arguments** — and not as an omission. The credential is the whole of the scope:
|
|
335
|
+
the endpoint takes no parameters, and which repositories are in the answer is decided by SpecGuard
|
|
336
|
+
from the person the key speaks for (owned, plus shared with them through a membership). A repository
|
|
337
|
+
they neither own nor were given access to never enters the response, so it cannot be filtered *in*
|
|
338
|
+
from this side either.
|
|
339
|
+
|
|
340
|
+
The body comes back as SpecGuard serves it — `{"repositories": […]}`, each entry carrying `id`,
|
|
341
|
+
`full_name`, `name`, `registered_at` and `role`, ordered by `full_name` ascending. The first four are
|
|
342
|
+
deliberately the same four fields, under the same names, that `get_repository_overview` serves in its
|
|
343
|
+
own `repository` block, so a client that reads one reads the other. `role` is `owner` or `member`:
|
|
344
|
+
the list mixes repositories this person owns with repositories somebody shared with them, and nothing
|
|
345
|
+
else tells them apart — read it before assuming a repository is one you may administer. An empty list
|
|
346
|
+
means no access, not an error.
|
|
347
|
+
|
|
348
|
+
**It reads a different key from `get_repository_overview`.** `SPECGUARD_USER_API_KEY` (`sgu_…`), not
|
|
349
|
+
`SPECGUARD_API_KEY` (`sgk_…`). SpecGuard refuses each credential in the other's place — the prefix
|
|
350
|
+
decides which table is consulted before any of them is read — so the two are not interchangeable and
|
|
351
|
+
setting one does not stand in for the other. Every message this tool produces names the variable
|
|
352
|
+
*it* reads, so a `401` here never sends you to check the key `get_repository_overview` uses.
|
|
353
|
+
|
|
354
|
+
Registering a repository is now `add_repository`, below — it reads the same `sgu_…` key and takes the
|
|
355
|
+
`full_name` this tool reports. Revoking keys and the rest of the user-scoped surface are still absent,
|
|
356
|
+
because their endpoints have not shipped: a tool here is a promise the agent will act on, so it waits
|
|
357
|
+
for the capability rather than the other way round.
|
|
358
|
+
|
|
359
|
+
### `add_repository`
|
|
360
|
+
|
|
361
|
+
Registers a GitHub repository with SpecGuard for the person behind `SPECGUARD_USER_API_KEY`, and
|
|
362
|
+
returns the repository together with its **first CI API key** — the `sgk_…` key that repository's CI
|
|
363
|
+
will use to ingest runs, minted in the same call so a fresh registration is usable without a second
|
|
364
|
+
trip through the browser.
|
|
365
|
+
|
|
366
|
+
| argument | |
|
|
367
|
+
| --- | --- |
|
|
368
|
+
| `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 |
|
|
369
|
+
|
|
370
|
+
The body comes back as SpecGuard serves it: a `repository` block (`id`, `full_name`, `name`,
|
|
371
|
+
`registered_at` — deliberately the same four fields `get_repository_overview` serves in its own
|
|
372
|
+
`repository` block) and an `api_key` block (`name`, `token`, `hint`, `created_at`).
|
|
373
|
+
|
|
374
|
+
> ⚠️ **`api_key.token` is shown once and never again.** Nothing stores it and no endpoint can
|
|
375
|
+
> re-serve it. Capture it from this response — an agent should hand it straight to the person it is
|
|
376
|
+
> working for. A key that is lost is replaced from SpecGuard's API-keys page in a browser, not from
|
|
377
|
+
> here.
|
|
378
|
+
|
|
379
|
+
> ⚠️ **This tool is not idempotent, and it writes.** If the call exceeds `SPECGUARD_TIMEOUT_MS` the
|
|
380
|
+
> bridge gives up, but the registration may still have succeeded on the server — taking its one-time
|
|
381
|
+
> token into a response nobody received. The retry is then refused with `has already been taken`,
|
|
382
|
+
> which is the honest answer rather than a bug. Do not retry a timeout blindly; check
|
|
383
|
+
> `list_repositories`, and recover the key in the browser.
|
|
384
|
+
|
|
385
|
+
**It needs a current record of your GitHub permissions, and only a browser creates one.** SpecGuard
|
|
386
|
+
decides whether you may register a repository from a stored grant, and fails closed when that grant
|
|
387
|
+
is missing or stale — which is every person who has not signed in and connected GitHub recently. That
|
|
388
|
+
refusal arrives as SpecGuard's own sentence, verbatim, naming the fix: *sign in to SpecGuard in a
|
|
389
|
+
browser and reconnect GitHub, then try again*. No argument to this tool substitutes for it. The same
|
|
390
|
+
path carries the other refusals — a repository the SpecGuard GitHub App is not installed on, one you
|
|
391
|
+
do not administer, one already registered.
|
|
392
|
+
|
|
393
|
+
**The `org/repo` format is not re-checked here.** This bridge verifies only that you passed a
|
|
394
|
+
non-blank string; SpecGuard validates the name and refuses an unusable one in its own words. A second
|
|
395
|
+
format rule on this side would be free to drift from the one that actually decides, and would surface
|
|
396
|
+
as this bridge rejecting a name the platform would have accepted.
|
|
397
|
+
|
|
398
|
+
It reads `SPECGUARD_USER_API_KEY` (`sgu_…`), the same credential as `list_repositories` and a
|
|
399
|
+
different one from the `sgk_…` key `get_repository_overview` uses.
|
|
400
|
+
|
|
300
401
|
## How it works
|
|
301
402
|
|
|
302
403
|
```
|
|
@@ -306,11 +407,15 @@ agent ⇄ specguard-mcp ⇄ SpecGuard (HTTP, the same API the dashbo
|
|
|
306
407
|
|
|
307
408
|
The bridge is a **thin client**: it shells out and it calls the API, and it re-implements neither.
|
|
308
409
|
It carries no knowledge of the OpenTestIntent schema, holds no copy of the linter's rules, and
|
|
309
|
-
reshapes no response —
|
|
410
|
+
reshapes no response — every tool returns the shape of the capability it wraps, so a field added
|
|
310
411
|
upstream reaches the agent without a release here.
|
|
311
412
|
|
|
312
|
-
Authorization and project scoping are enforced by SpecGuard, never by this bridge, using
|
|
313
|
-
`sgk_…` keys CI uses to ingest runs
|
|
413
|
+
Authorization and project scoping are enforced by SpecGuard, never by this bridge, using keys you
|
|
414
|
+
issue there — the same `sgk_…` keys CI uses to ingest runs, and, for the tools that answer to a
|
|
415
|
+
person rather than to a repository, an `sgu_…` user key. Which of the two a request may carry is
|
|
416
|
+
SpecGuard's decision and it is taken from the token's prefix, before any credential is looked up, so
|
|
417
|
+
the bridge cannot widen either one's reach: it forwards the key the tool's own variable holds and
|
|
418
|
+
reports what came back. It adds no credentials of its own and stores nothing.
|
|
314
419
|
|
|
315
420
|
No argument ever reaches a shell: subprocesses are spawned with an argument list, so a path from a
|
|
316
421
|
model is a path that does not exist rather than a command.
|
|
@@ -322,11 +427,13 @@ The toolset fills in as more of SpecGuard lands. Adding one is two mechanical ed
|
|
|
322
427
|
1. a new file under `src/tools/` that default-exports a `ToolDefinition`;
|
|
323
428
|
2. one entry appended to the array in `src/tools/index.ts`.
|
|
324
429
|
|
|
325
|
-
There is no third — no third *wiring* edit, at least: every tool also earns a `### ` section
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
430
|
+
There is no third — no third *wiring* edit, at least: every tool also earns a `### ` section above,
|
|
431
|
+
and every argument earns a row in that section's table, since this README ships as the package's
|
|
432
|
+
published documentation, and `test/readme.test.ts` derives that obligation from the registry so a
|
|
433
|
+
missing section or an undocumented parameter fails the suite. A tool that genuinely takes no
|
|
434
|
+
arguments (`list_repositories` is the first) still earns the section, and is named in that file's
|
|
435
|
+
`ARGUMENT_LESS_TOOLS` — a deliberate line to add, rather than a floor relaxed for everyone.
|
|
436
|
+
`src/server.ts` iterates that array and contains no per-tool code — no `switch`, no hard-coded name — and everything a tool
|
|
330
437
|
touches the world with (config, subprocesses, `fetch`) is injected, so a new tool is testable
|
|
331
438
|
without a live deployment for free. The property tests in `test/tools/registry.test.ts` run over
|
|
332
439
|
whatever the registry holds, so a tool added later is checked by tests written today.
|
|
@@ -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"}
|
package/dist/src/config.d.ts
CHANGED
|
@@ -26,6 +26,28 @@
|
|
|
26
26
|
* too rather than left as a silent no-op for anyone who follows the ticket.
|
|
27
27
|
* `SPECGUARD_ENDPOINT` wins when both are set and disagree, because it is the
|
|
28
28
|
* one the rest of the toolchain is already reading.
|
|
29
|
+
*
|
|
30
|
+
* == TWO KEY VARIABLES, because SpecGuard has two credentials that refuse each
|
|
31
|
+
* == other
|
|
32
|
+
*
|
|
33
|
+
* `Api::BaseController` discriminates on the token's PREFIX *before any table is
|
|
34
|
+
* read*, and answers 401 on a mismatch without a lookup: `sgk_` names ONE
|
|
35
|
+
* repository (`GET /api/v1/repository`, `POST /api/v1/ingest`), `sgu_` names a
|
|
36
|
+
* PERSON (`GET /api/v1/repositories`). `UserApiKey::TOKEN_PREFIX` says the two
|
|
37
|
+
* prefixes are deliberately the same length so neither can be a prefix of the
|
|
38
|
+
* other — the mutual refusal is designed, not incidental.
|
|
39
|
+
*
|
|
40
|
+
* One variable therefore cannot serve both: whichever kind it holds, the tools
|
|
41
|
+
* needing the other kind 401. So there are two, read independently, and neither
|
|
42
|
+
* is required — an operator who only ever calls the repository tool sets only
|
|
43
|
+
* `SPECGUARD_API_KEY`, exactly as before this existed. Prefix-dispatching over a
|
|
44
|
+
* single variable was the alternative and it cannot work: an operator wanting
|
|
45
|
+
* both kinds of tool needs both keys present at once.
|
|
46
|
+
*
|
|
47
|
+
* Which variable a tool reads is then carried onto `ApiConfig` alongside the
|
|
48
|
+
* value — see `Credential` — for the same reason `endpointVariable` is: a
|
|
49
|
+
* diagnostic that names the wrong variable sends an operator to fix something
|
|
50
|
+
* they never set.
|
|
29
51
|
*/
|
|
30
52
|
export interface Config {
|
|
31
53
|
/** SpecGuard deployment root, trailing slash stripped. `undefined` when unset. */
|
|
@@ -40,8 +62,18 @@ export interface Config {
|
|
|
40
62
|
* speak the one that was used.
|
|
41
63
|
*/
|
|
42
64
|
readonly endpointVariable: EndpointVariable | undefined;
|
|
43
|
-
/** An `sgk_…` API key
|
|
65
|
+
/** An `sgk_…` repository API key, from `SPECGUARD_API_KEY`. `undefined` when unset. */
|
|
44
66
|
readonly apiKey: string | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* An `sgu_…` user API key, from `SPECGUARD_USER_API_KEY`. `undefined` when unset.
|
|
69
|
+
*
|
|
70
|
+
* A SECOND slot rather than a second meaning for the first one — see the note
|
|
71
|
+
* at the top of this file. It is minted from the account page
|
|
72
|
+
* (`/account`) and speaks for a person; the repository key is minted per
|
|
73
|
+
* repository and speaks for one repository. Neither deployment endpoint will
|
|
74
|
+
* accept the other's token, so the two values live in two places here too.
|
|
75
|
+
*/
|
|
76
|
+
readonly userApiKey: string | undefined;
|
|
45
77
|
/**
|
|
46
78
|
* The command that runs the `@intent` linter, already tokenised.
|
|
47
79
|
*
|
|
@@ -76,27 +108,85 @@ export interface ApiConfig {
|
|
|
76
108
|
*/
|
|
77
109
|
readonly endpointVariable: EndpointVariable;
|
|
78
110
|
readonly apiKey: string;
|
|
111
|
+
/**
|
|
112
|
+
* WHICH credential `apiKey` is — the variable it was read from, the prefix
|
|
113
|
+
* that variable is expected to hold, and how to say both in a sentence.
|
|
114
|
+
*
|
|
115
|
+
* Exactly the treatment `endpointVariable` above gets, applied to the other
|
|
116
|
+
* half, and for the same reason. `describeFailure` in
|
|
117
|
+
* `support/specguard-api.ts` used to hardcode "SPECGUARD_API_KEY must be an
|
|
118
|
+
* sgk_… key … keys are per-repository" into every 401, so the first
|
|
119
|
+
* user-scoped tool routed through `getJson` would have inherited three
|
|
120
|
+
* sentences that are all false of it — naming a variable its operator may
|
|
121
|
+
* never have set. Carrying the answer rather than re-deriving it per tool is
|
|
122
|
+
* what makes the next credential-scoped tool inherit correct naming the same
|
|
123
|
+
* way it inherits the URL check.
|
|
124
|
+
*/
|
|
125
|
+
readonly credential: Credential;
|
|
79
126
|
readonly requestTimeoutMs: number;
|
|
80
127
|
}
|
|
81
128
|
export type EndpointVariable = "SPECGUARD_ENDPOINT" | "SPECGUARD_URL";
|
|
129
|
+
export type ApiKeyVariable = "SPECGUARD_API_KEY" | "SPECGUARD_USER_API_KEY";
|
|
130
|
+
/**
|
|
131
|
+
* One of SpecGuard's two credential kinds, described well enough that a message
|
|
132
|
+
* about it can be written without knowing which one it is.
|
|
133
|
+
*
|
|
134
|
+
* The two prose fields are sentence FRAGMENTS rather than whole messages on
|
|
135
|
+
* purpose: the surrounding wording — "is not set in the MCP server's
|
|
136
|
+
* environment", "SpecGuard rejected the API key (401)" — is the same for both
|
|
137
|
+
* kinds and is written once, at the site that knows the situation. Only the
|
|
138
|
+
* parts that genuinely differ between an `sgk_` key and an `sgu_` key live
|
|
139
|
+
* here.
|
|
140
|
+
*/
|
|
141
|
+
export interface Credential {
|
|
142
|
+
/** The environment variable this kind of key is read from. */
|
|
143
|
+
readonly variable: ApiKeyVariable;
|
|
144
|
+
/** The prefix SpecGuard requires of it, checked before any table is read. */
|
|
145
|
+
readonly prefix: "sgk_" | "sgu_";
|
|
146
|
+
/** Completes "… issued from ___" in a message about the variable being unset. */
|
|
147
|
+
readonly issuedFrom: string;
|
|
148
|
+
/** Completes "… issued by <deployment> ___" in a message about a 401. */
|
|
149
|
+
readonly rejection: string;
|
|
150
|
+
}
|
|
82
151
|
/**
|
|
83
|
-
*
|
|
152
|
+
* The `sgk_` key: one repository, and the same variable CI already sets.
|
|
84
153
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
154
|
+
* The 401 wording is unchanged from when this was the only credential — it is
|
|
155
|
+
* accurate about this kind, and the reason a second kind exists is precisely
|
|
156
|
+
* that it was never accurate about the other.
|
|
157
|
+
*/
|
|
158
|
+
export declare const REPOSITORY_CREDENTIAL: Credential;
|
|
159
|
+
/**
|
|
160
|
+
* The `sgu_` key: a person, and a variable nothing else in the toolchain reads.
|
|
88
161
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
*
|
|
97
|
-
*
|
|
162
|
+
* `specguard-rspec` ships runs with an `sgk_` key and has no notion of this one,
|
|
163
|
+
* so an operator who already has CI reporting to SpecGuard does NOT already
|
|
164
|
+
* have this variable — which is why the message says where to mint one rather
|
|
165
|
+
* than assuming it is lying around.
|
|
166
|
+
*/
|
|
167
|
+
export declare const USER_CREDENTIAL: Credential;
|
|
168
|
+
/**
|
|
169
|
+
* What a tool needing the `sgk_` REPOSITORY key requires — the endpoint and
|
|
170
|
+
* `SPECGUARD_API_KEY`.
|
|
171
|
+
*
|
|
172
|
+
* Unchanged in name and in behaviour: every tool that reached the deployment
|
|
173
|
+
* before this file grew a second credential calls exactly this, and gets exactly
|
|
174
|
+
* what it got.
|
|
98
175
|
*/
|
|
99
176
|
export declare function requireApiConfig(config: Config): ApiConfig;
|
|
177
|
+
/**
|
|
178
|
+
* What a tool needing the `sgu_` USER key requires — the endpoint and
|
|
179
|
+
* `SPECGUARD_USER_API_KEY`.
|
|
180
|
+
*
|
|
181
|
+
* A sibling rather than a flag on `requireApiConfig`, which is the shape
|
|
182
|
+
* `loadConfig`'s note at the top of this file already sanctioned: a tool asks
|
|
183
|
+
* for what IT needs, so a tool needing neither key is unaffected and startup
|
|
184
|
+
* still validates nothing. The two differ only in which value and which
|
|
185
|
+
* `Credential` they pass to the one implementation below, so a fix to the
|
|
186
|
+
* diagnostics reaches both and they cannot drift into describing the same
|
|
187
|
+
* situation differently.
|
|
188
|
+
*/
|
|
189
|
+
export declare function requireUserApiConfig(config: Config): ApiConfig;
|
|
100
190
|
/**
|
|
101
191
|
* Splits a configured command into argv WITHOUT a shell, honouring single and
|
|
102
192
|
* double quotes so a path with a space survives.
|
package/dist/src/config.js
CHANGED
|
@@ -24,21 +24,81 @@ export function loadConfig(env = process.env) {
|
|
|
24
24
|
endpoint: endpointVariable === undefined ? undefined : normaliseEndpoint(env[endpointVariable]),
|
|
25
25
|
endpointVariable,
|
|
26
26
|
apiKey: presence(env["SPECGUARD_API_KEY"]),
|
|
27
|
+
userApiKey: presence(env["SPECGUARD_USER_API_KEY"]),
|
|
27
28
|
lintCommand: lintCommand.length > 0 ? lintCommand : DEFAULT_LINT_COMMAND,
|
|
28
29
|
requestTimeoutMs: positiveInteger(env["SPECGUARD_TIMEOUT_MS"]) ?? DEFAULT_REQUEST_TIMEOUT_MS,
|
|
29
30
|
};
|
|
30
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* The `sgk_` key: one repository, and the same variable CI already sets.
|
|
34
|
+
*
|
|
35
|
+
* The 401 wording is unchanged from when this was the only credential — it is
|
|
36
|
+
* accurate about this kind, and the reason a second kind exists is precisely
|
|
37
|
+
* that it was never accurate about the other.
|
|
38
|
+
*/
|
|
39
|
+
export const REPOSITORY_CREDENTIAL = {
|
|
40
|
+
variable: "SPECGUARD_API_KEY",
|
|
41
|
+
prefix: "sgk_",
|
|
42
|
+
issuedFrom: "issued from its API keys page",
|
|
43
|
+
rejection: "for the repository you are asking about — keys are per-repository, and a " +
|
|
44
|
+
"revoked key reads the same as a wrong one",
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* The `sgu_` key: a person, and a variable nothing else in the toolchain reads.
|
|
48
|
+
*
|
|
49
|
+
* `specguard-rspec` ships runs with an `sgk_` key and has no notion of this one,
|
|
50
|
+
* so an operator who already has CI reporting to SpecGuard does NOT already
|
|
51
|
+
* have this variable — which is why the message says where to mint one rather
|
|
52
|
+
* than assuming it is lying around.
|
|
53
|
+
*/
|
|
54
|
+
export const USER_CREDENTIAL = {
|
|
55
|
+
variable: "SPECGUARD_USER_API_KEY",
|
|
56
|
+
prefix: "sgu_",
|
|
57
|
+
issuedFrom: "issued from your account page",
|
|
58
|
+
rejection: "for your own SpecGuard account — a user key speaks for a person and lists what " +
|
|
59
|
+
"that person may open, an sgk_… repository key is refused here without a lookup, " +
|
|
60
|
+
"and a revoked key reads the same as a wrong one",
|
|
61
|
+
};
|
|
31
62
|
/**
|
|
32
63
|
* The name to speak when no variable was set at all — the message is telling
|
|
33
64
|
* someone to set one, and this is the spelling the rest of the toolchain reads.
|
|
34
65
|
*/
|
|
35
66
|
const DEFAULT_ENDPOINT_VARIABLE = "SPECGUARD_ENDPOINT";
|
|
67
|
+
/**
|
|
68
|
+
* What a tool needing the `sgk_` REPOSITORY key requires — the endpoint and
|
|
69
|
+
* `SPECGUARD_API_KEY`.
|
|
70
|
+
*
|
|
71
|
+
* Unchanged in name and in behaviour: every tool that reached the deployment
|
|
72
|
+
* before this file grew a second credential calls exactly this, and gets exactly
|
|
73
|
+
* what it got.
|
|
74
|
+
*/
|
|
75
|
+
export function requireApiConfig(config) {
|
|
76
|
+
return requireCredentialledApiConfig(config, config.apiKey, REPOSITORY_CREDENTIAL);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* What a tool needing the `sgu_` USER key requires — the endpoint and
|
|
80
|
+
* `SPECGUARD_USER_API_KEY`.
|
|
81
|
+
*
|
|
82
|
+
* A sibling rather than a flag on `requireApiConfig`, which is the shape
|
|
83
|
+
* `loadConfig`'s note at the top of this file already sanctioned: a tool asks
|
|
84
|
+
* for what IT needs, so a tool needing neither key is unaffected and startup
|
|
85
|
+
* still validates nothing. The two differ only in which value and which
|
|
86
|
+
* `Credential` they pass to the one implementation below, so a fix to the
|
|
87
|
+
* diagnostics reaches both and they cannot drift into describing the same
|
|
88
|
+
* situation differently.
|
|
89
|
+
*/
|
|
90
|
+
export function requireUserApiConfig(config) {
|
|
91
|
+
return requireCredentialledApiConfig(config, config.userApiKey, USER_CREDENTIAL);
|
|
92
|
+
}
|
|
36
93
|
/**
|
|
37
94
|
* Both halves or a legible failure — never one half and a surprise later.
|
|
38
95
|
*
|
|
39
96
|
* Reported together rather than one at a time: an operator who set neither
|
|
40
97
|
* should learn that in one round trip instead of fixing a variable, re-calling,
|
|
41
|
-
* and being told about the next one.
|
|
98
|
+
* and being told about the next one. That property is why the two `require*`
|
|
99
|
+
* entry points share this body rather than each doing their own presence check:
|
|
100
|
+
* "together" has to mean the endpoint AND whichever key the calling tool needs,
|
|
101
|
+
* and a per-tool check would report them one at a time again.
|
|
42
102
|
*
|
|
43
103
|
* The endpoint is also PARSED here, not merely counted as present. It is spent
|
|
44
104
|
* later inside `new URL(...)` in the HTTP client, where a malformed value throws
|
|
@@ -49,25 +109,36 @@ const DEFAULT_ENDPOINT_VARIABLE = "SPECGUARD_ENDPOINT";
|
|
|
49
109
|
* truth, and it sends an agent looking in the one place the problem is not.
|
|
50
110
|
* Validating here rather than at the call site is deliberate: every HTTP-backed
|
|
51
111
|
* tool added later comes through this function and inherits the check.
|
|
112
|
+
*
|
|
113
|
+
* The KEY is deliberately NOT validated against `credential.prefix` here. The
|
|
114
|
+
* deployment is the authority on whether a token is acceptable — it checks the
|
|
115
|
+
* prefix, then the digest, then whether the key is revoked — and a second,
|
|
116
|
+
* weaker copy of the first third of that rule on this side would refuse a token
|
|
117
|
+
* SpecGuard would have accepted the moment the platform mints a third prefix.
|
|
118
|
+
* The prefix is carried so a MESSAGE can name it, not so this file can enforce
|
|
119
|
+
* it; the 401 branch of `describeFailure` is where a wrong-kind key is
|
|
120
|
+
* diagnosed, with the deployment's own verdict in hand.
|
|
52
121
|
*/
|
|
53
|
-
|
|
122
|
+
function requireCredentialledApiConfig(config, apiKey, credential) {
|
|
54
123
|
const endpointVariable = config.endpointVariable ?? DEFAULT_ENDPOINT_VARIABLE;
|
|
55
124
|
const missing = [];
|
|
56
125
|
if (config.endpoint === undefined)
|
|
57
126
|
missing.push(endpointVariable);
|
|
58
|
-
if (
|
|
59
|
-
missing.push(
|
|
127
|
+
if (apiKey === undefined)
|
|
128
|
+
missing.push(credential.variable);
|
|
60
129
|
if (missing.length > 0) {
|
|
61
130
|
throw new ConfigError(`This tool talks to a SpecGuard deployment, and ${missing.join(" and ")} ` +
|
|
62
131
|
`${missing.length === 1 ? "is" : "are"} not set in the MCP server's environment. ` +
|
|
63
132
|
"Set them in your MCP client's server config " +
|
|
64
|
-
`(${endpointVariable} is your deployment's root URL,
|
|
65
|
-
|
|
133
|
+
`(${endpointVariable} is your deployment's root URL, ${credential.variable} ` +
|
|
134
|
+
`an ${credential.prefix}… key ${credential.issuedFrom}). ` +
|
|
135
|
+
"Tools that do not reach the deployment are unaffected.");
|
|
66
136
|
}
|
|
67
137
|
return {
|
|
68
138
|
endpoint: requireHttpUrl(config.endpoint, endpointVariable),
|
|
69
139
|
endpointVariable,
|
|
70
|
-
apiKey:
|
|
140
|
+
apiKey: apiKey,
|
|
141
|
+
credential,
|
|
71
142
|
requestTimeoutMs: config.requestTimeoutMs,
|
|
72
143
|
};
|
|
73
144
|
}
|
package/dist/src/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AA4F1C,MAAM,CAAC,MAAM,oBAAoB,GAAsB,CAAC,gBAAgB,CAAC,CAAC;AAC1E,MAAM,CAAC,MAAM,0BAA0B,GAAG,MAAM,CAAC;AAEjD,2EAA2E;AAC3E,MAAM,UAAU,UAAU,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC7D,MAAM,WAAW,GAAG,QAAQ,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC,CAAC;IAC5D,6EAA6E;IAC7E,qEAAqE;IACrE,4EAA4E;IAC5E,8EAA8E;IAC9E,0EAA0E;IAC1E,6EAA6E;IAC7E,8EAA8E;IAC9E,8EAA8E;IAC9E,6EAA6E;IAC7E,mEAAmE;IACnE,8EAA8E;IAC9E,MAAM,gBAAgB,GACpB,QAAQ,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC,KAAK,SAAS;QAC/C,CAAC,CAAC,oBAAoB;QACtB,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,KAAK,SAAS;YAC5C,CAAC,CAAC,eAAe;YACjB,CAAC,CAAC,SAAS,CAAC;IAElB,OAAO;QACL,QAAQ,EAAE,gBAAgB,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,iBAAiB,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;QAC/F,gBAAgB;QAChB,MAAM,EAAE,QAAQ,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;QAC1C,UAAU,EAAE,QAAQ,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAC;QACnD,WAAW,EAAE,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,oBAAoB;QACxE,gBAAgB,EAAE,eAAe,CAAC,GAAG,CAAC,sBAAsB,CAAC,CAAC,IAAI,0BAA0B;KAC7F,CAAC;AACJ,CAAC;AA+DD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAe;IAC/C,QAAQ,EAAE,mBAAmB;IAC7B,MAAM,EAAE,MAAM;IACd,UAAU,EAAE,+BAA+B;IAC3C,SAAS,EACP,2EAA2E;QAC3E,2CAA2C;CAC9C,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,eAAe,GAAe;IACzC,QAAQ,EAAE,wBAAwB;IAClC,MAAM,EAAE,MAAM;IACd,UAAU,EAAE,+BAA+B;IAC3C,SAAS,EACP,iFAAiF;QACjF,kFAAkF;QAClF,iDAAiD;CACpD,CAAC;AAEF;;;GAGG;AACH,MAAM,yBAAyB,GAAqB,oBAAoB,CAAC;AAEzE;;;;;;;GAOG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAc;IAC7C,OAAO,6BAA6B,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAC;AACrF,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,oBAAoB,CAAC,MAAc;IACjD,OAAO,6BAA6B,CAAC,MAAM,EAAE,MAAM,CAAC,UAAU,EAAE,eAAe,CAAC,CAAC;AACnF,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,SAAS,6BAA6B,CACpC,MAAc,EACd,MAA0B,EAC1B,UAAsB;IAEtB,MAAM,gBAAgB,GAAG,MAAM,CAAC,gBAAgB,IAAI,yBAAyB,CAAC;IAE9E,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS;QAAE,OAAO,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;IAClE,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;IAE5D,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,WAAW,CACnB,kDAAkD,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG;YACxE,GAAG,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,4CAA4C;YAClF,8CAA8C;YAC9C,IAAI,gBAAgB,mCAAmC,UAAU,CAAC,QAAQ,GAAG;YAC7E,MAAM,UAAU,CAAC,MAAM,SAAS,UAAU,CAAC,UAAU,KAAK;YAC1D,wDAAwD,CAC3D,CAAC;IACJ,CAAC;IAED,OAAO;QACL,QAAQ,EAAE,cAAc,CAAC,MAAM,CAAC,QAAkB,EAAE,gBAAgB,CAAC;QACrE,gBAAgB;QAChB,MAAM,EAAE,MAAgB;QACxB,UAAU;QACV,gBAAgB,EAAE,MAAM,CAAC,gBAAgB;KAC1C,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,cAAc,CAAC,QAAgB,EAAE,IAAsB;IAC9D,IAAI,MAAuB,CAAC;IAC5B,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,GAAG,SAAS,CAAC;IACrB,CAAC;IAED,IAAI,MAAM,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,QAAQ,KAAK,OAAO,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,CAAC,EAAE,CAAC;QAC1F,MAAM,IAAI,WAAW,CACnB,GAAG,IAAI,yBAAyB,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,8BAA8B;YACpF,2DAA2D;YAC3D,kFAAkF;YAClF,iDAAiD,IAAI,+BAA+B;YACpF,gEAAgE,CACnE,CAAC;IACJ,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;GAKG;AACH,SAAS,iBAAiB,CAAC,GAAuB;IAChD,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;IAC5B,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AACrE,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,QAAQ,CAAC,GAAuB;IACvC,MAAM,OAAO,GAAG,GAAG,EAAE,IAAI,EAAE,CAAC;IAC5B,OAAO,OAAO,KAAK,SAAS,IAAI,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;AACvE,CAAC;AAED,SAAS,eAAe,CAAC,GAAuB;IAC9C,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;IACpC,OAAO,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AACtE,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,QAAQ,CAAC,GAAuB;IAC9C,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;IAC5B,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IAEnC,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,OAAO,GAAG,EAAE,CAAC;IACjB,IAAI,KAA4B,CAAC;IACjC,IAAI,OAAO,GAAG,KAAK,CAAC;IAEpB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,IAAI,IAAI,KAAK,KAAK;gBAAE,KAAK,GAAG,SAAS,CAAC;;gBACjC,OAAO,IAAI,IAAI,CAAC;YACrB,SAAS;QACX,CAAC;QAED,IAAI,IAAI,KAAK,GAAG,IAAI,IAAI,KAAK,GAAG,EAAE,CAAC;YACjC,KAAK,GAAG,IAAI,CAAC;YACb,OAAO,GAAG,IAAI,CAAC;YACf,SAAS;QACX,CAAC;QAED,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YACpB,IAAI,OAAO;gBAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YAClC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,GAAG,KAAK,CAAC;YAChB,SAAS;QACX,CAAC;QAED,OAAO,IAAI,IAAI,CAAC;QAChB,OAAO,GAAG,IAAI,CAAC;IACjB,CAAC;IAED,IAAI,OAAO;QAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAElC,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { createServer, SERVER_NAME, SERVER_VERSION, type CreateServerOptions } from "./server.js";
|
|
2
|
-
export { loadConfig, requireApiConfig, tokenise, type ApiConfig, type Config } from "./config.js";
|
|
2
|
+
export { loadConfig, requireApiConfig, requireUserApiConfig, tokenise, type ApiConfig, type ApiKeyVariable, type Config, type Credential, } from "./config.js";
|
|
3
3
|
export { ApiError, ArgumentError, CommandError, ConfigError, SpecGuardMcpError } from "./errors.js";
|
|
4
4
|
export { TOOLS } from "./tools/index.js";
|
|
5
5
|
export type { ToolContext, ToolDefinition, ToolResult } from "./tools/types.js";
|
package/dist/src/index.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
export { createServer, SERVER_NAME, SERVER_VERSION } from "./server.js";
|
|
2
|
-
|
|
2
|
+
// `requireUserApiConfig` is re-exported beside `requireApiConfig` rather than
|
|
3
|
+
// left behind it: the two are one seam with two credentials, and a consumer that
|
|
4
|
+
// can name only half of it would have to deep-import past this entrypoint to
|
|
5
|
+
// reach the other — the same unnameable-but-typed state `test/index.test.ts`
|
|
6
|
+
// exists to keep out of the error taxonomy.
|
|
7
|
+
export { loadConfig, requireApiConfig, requireUserApiConfig, tokenise, } from "./config.js";
|
|
3
8
|
export { ApiError, ArgumentError, CommandError, ConfigError, SpecGuardMcpError } from "./errors.js";
|
|
4
9
|
export { TOOLS } from "./tools/index.js";
|
|
5
10
|
//# sourceMappingURL=index.js.map
|
package/dist/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,cAAc,EAA4B,MAAM,aAAa,CAAC;AAClG,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,cAAc,EAA4B,MAAM,aAAa,CAAC;AAClG,8EAA8E;AAC9E,iFAAiF;AACjF,6EAA6E;AAC7E,6EAA6E;AAC7E,4CAA4C;AAC5C,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,oBAAoB,EACpB,QAAQ,GAKT,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACpG,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC"}
|