@cat-factory/mcp-server 0.2.0 → 0.3.0

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 CHANGED
@@ -10,6 +10,17 @@ It is a **facade**, not a client. Every tool is one call on
10
10
 
11
11
  ## Run it
12
12
 
13
+ With Claude Code, in one line:
14
+
15
+ ```sh
16
+ claude mcp add cat-factory \
17
+ --env CAT_FACTORY_BASE_URL=https://cat-factory.example.com \
18
+ --env CAT_FACTORY_API_KEY_FILE=$HOME/.config/cat-factory/api-key \
19
+ -- npx -y @cat-factory/mcp-server
20
+ ```
21
+
22
+ Or in any host's own config format:
23
+
13
24
  ```jsonc
14
25
  // An MCP host's server config
15
26
  {
@@ -19,7 +30,7 @@ It is a **facade**, not a client. Every tool is one call on
19
30
  "args": ["-y", "@cat-factory/mcp-server"],
20
31
  "env": {
21
32
  "CAT_FACTORY_BASE_URL": "https://cat-factory.example.com",
22
- "CAT_FACTORY_API_KEY": "cf_live_...",
33
+ "CAT_FACTORY_API_KEY_FILE": "/home/you/.config/cat-factory/api-key",
23
34
  },
24
35
  },
25
36
  },
@@ -30,19 +41,76 @@ Mint the key from the deployment (`backend/docs/public-api.md` §Setup) and give
30
41
  scope that does the job**: `read ⊂ write ⊂ decide ⊂ admin`. Every tool is scoped to the key's
31
42
  workspace, and the key is what actually decides what a model can do here.
32
43
 
33
- | Variable | Meaning |
34
- | ---------------------------------- | --------------------------------------------------------------- |
35
- | `CAT_FACTORY_BASE_URL` | The deployment's origin. Required. |
36
- | `CAT_FACTORY_API_KEY` | A public-API key. Required. |
37
- | `CAT_FACTORY_MCP_GROUPS` | Comma-separated resource groups to expose. Unset all of them. |
38
- | `CAT_FACTORY_MCP_READ_ONLY` | `true` expose only the tools that change nothing. |
39
- | `CAT_FACTORY_MCP_MAX_RESULT_CHARS` | Ceiling on one tool result. Default 100,000. |
40
- | `CAT_FACTORY_MCP_TIMEOUT_MS` | Per-request deadline passed to the SDK. `0` disables it. |
41
- | `CAT_FACTORY_MCP_MAX_RETRIES` | Retries for a retriable failure, passed to the SDK. |
42
-
43
- Missing credentials, an unknown group name and a non-numeric ceiling all **fail at startup**. A
44
- server that comes up and then fails every call is reported by the host as connected, and the model
45
- spends turns discovering otherwise.
44
+ | Variable | Meaning |
45
+ | ---------------------------------- | -------------------------------------------------------------------------- |
46
+ | `CAT_FACTORY_BASE_URL` | The deployment's origin. Required. |
47
+ | `CAT_FACTORY_API_KEY` | A public-API key. Required, unless the file below is given. |
48
+ | `CAT_FACTORY_API_KEY_FILE` | A file holding the key instead. Either one, never both. |
49
+ | `CAT_FACTORY_MCP_GROUPS` | Comma-separated resource groups to expose. Unset all of them. |
50
+ | `CAT_FACTORY_MCP_TOOLS` | Comma-separated tool names to expose. Unset ⇒ all the other filters leave. |
51
+ | `CAT_FACTORY_MCP_EXCLUDE_TOOLS` | Comma-separated tool names to withhold, applied after every other filter. |
52
+ | `CAT_FACTORY_MCP_READ_ONLY` | `true` expose only the tools that change nothing. |
53
+ | `CAT_FACTORY_MCP_MAX_RESULT_CHARS` | Ceiling on one tool result. Default 100,000. |
54
+ | `CAT_FACTORY_MCP_TIMEOUT_MS` | Per-request deadline passed to the SDK. `0` disables it. |
55
+ | `CAT_FACTORY_MCP_MAX_RETRIES` | Retries for a retriable failure, passed to the SDK. |
56
+
57
+ Missing credentials, an unknown group or tool name, a filter combination that would expose nothing,
58
+ and a non-numeric ceiling all **fail at startup**. A server that comes up and then fails every call
59
+ is reported by the host as connected, and the model spends turns discovering otherwise.
60
+
61
+ ### Keep the key out of the host's config
62
+
63
+ `CAT_FACTORY_API_KEY_FILE` names a file to read the key from. A stdio server's environment IS the
64
+ host's config file, so the inline variable means a long-lived credential in plaintext in a home
65
+ directory, readable by everything that can read that file and present in every backup and every
66
+ screen share of it. A path is not a secret, so pointing at one lets the key live somewhere locked
67
+ down (`chmod 600`, a mounted secret, a secrets-manager sidecar's drop).
68
+
69
+ Setting **both** is refused rather than resolved by precedence: two live sources for one credential
70
+ means a rotation can land on the half nobody reads, and the deployment goes on working with the old
71
+ key right up until it is revoked.
72
+
73
+ ### Choosing what a model can reach
74
+
75
+ Three filters, narrowest last, all of them a convenience rather than a boundary (the key's scope is
76
+ the boundary):
77
+
78
+ - `CAT_FACTORY_MCP_GROUPS` is the coarse unit an operator thinks in: "no debug tools on this one".
79
+ - `CAT_FACTORY_MCP_TOOLS` exposes an explicitly chosen set. Precise, but it has to be re-edited
80
+ whenever `/api/v1` grows, and a forgotten edit silently withholds the new capability.
81
+ - `CAT_FACTORY_MCP_EXCLUDE_TOOLS` withholds named tools and keeps admitting everything else,
82
+ including tools added later. This is the one to reach for to keep ONE capability away from a model:
83
+ `CAT_FACTORY_MCP_EXCLUDE_TOOLS=notifications_act` keeps the PR-merging tool away without costing
84
+ the inbox it belongs to.
85
+
86
+ Whatever is switched off, the server **says so in its instructions**, naming the withheld tools and
87
+ stating that the deployment still supports them. An unexplained absence reads to a model as a
88
+ platform that cannot do the thing, which it then reports to its user, or works around.
89
+
90
+ ## A worked flow
91
+
92
+ Create a task, run it, watch it, answer the park. This is the shape of nearly every session here:
93
+
94
+ 1. **`services_list`**: the board's service frames. A task is created under one, and the `serviceId`
95
+ comes from here rather than being guessable.
96
+ 2. **`pipelines_list`**: which pipelines a task can be started with, and which are headless-startable.
97
+ 3. **`tasks_create`** (`serviceId`, `body.title`, `body.description`, `body.taskType`) returns the
98
+ `taskId`. Nothing runs yet; this is a board card.
99
+ 4. **`tasks_start`** (`taskId`) **spends**: it begins a real agent run against a real repository, and
100
+ returns the `runId`. Confirm with the person first; the tool is annotated `destructiveHint`, so
101
+ most hosts will ask anyway.
102
+ 5. **`tasks_get_run`** (`runId`): poll it. There is no streaming tool (see below), an agent step takes
103
+ minutes, so poll every 15-30 seconds and say so instead of going quiet. Keep going until the
104
+ status is terminal or a decision is parked.
105
+ 6. **`decisions_list`** (`runId`): a run that stops advancing has usually PARKED on a human decision,
106
+ and it waits indefinitely by design. Answer it with the other `decisions_*` tools
107
+ (`decisions_incorporate`, `decisions_proceed`, `decisions_choose_fork`,
108
+ `decisions_resolve_judge`, …) or leave it for a person.
109
+ 7. **`notifications_list`**: the human-actionable tail, including the merge decision.
110
+ `notifications_act` can merge a pull request, which is the other tool that spends.
111
+
112
+ For a run against a supplied brief with no board card and nothing pushed to a repository, the
113
+ `jobs_*` group is the same loop in one step: `jobs_create` → poll `jobs_get` → `jobs_cancel`.
46
114
 
47
115
  To mount it on your own transport instead of stdio:
48
116
 
@@ -69,6 +137,15 @@ Concretely:
69
137
  contract the deployment validates against, so a tool cannot describe a request shape the server
70
138
  would reject. An OPEN vocabulary (a closed set plus a documented escape hatch, like `taskType`)
71
139
  is never narrowed to an `enum`, because that would refuse a value the server accepts.
140
+ - **The output schema is deliberately looser than the spec.** Every tool that answers with a JSON
141
+ object declares an `outputSchema` and returns `structuredContent` beside the text. But a caller's
142
+ own MCP client VALIDATES a result against that schema, and `/api/v1` is additive forever, so the
143
+ emitted version carries no `required`, no `enum`, no closed `anyOf` and no length or range bounds.
144
+ Each of those would be a way for an older copy of this package to reject a newer deployment's
145
+ honest answer. The known members of a vocabulary are stated in the field's description instead,
146
+ where a new member cannot invalidate them, and a UNION asserts nothing at all beyond its
147
+ discriminator in prose: not even `type`, since the variants a union gains later need not be
148
+ objects.
72
149
  - **Argument validation stops at the door.** Required path ids are checked so a request can be
73
150
  built at all; the body goes to the deployment, whose 422 names the field and is far more useful
74
151
  to a model than anything this layer could say.
@@ -107,18 +184,27 @@ quietly stale.
107
184
 
108
185
  ## Things a caller should know
109
186
 
110
- - **`readOnlyHint` is set from the HTTP method** and is what a host uses to decide what needs a
111
- human's confirmation. `destructiveHint` and `idempotentHint` are deliberately left unset rather
112
- than guessed: a `DELETE` here is both idempotent and destructive, and an unset hint gets a host's
113
- safe default where a wrong one gets its unsafe one.
187
+ - **`readOnlyHint` is set from the HTTP method.** `destructiveHint` and `idempotentHint` are set on
188
+ the operations whose consequence is real money or a merged pull request (`tasks_start`,
189
+ `tasks_retry`, `jobs_create`, `notifications_act`, `tasks_delete`) and left UNSET everywhere else.
190
+ That asymmetry is deliberate: the protocol's default for an unset hint is already the cautious one,
191
+ so a blanket `destructiveHint: false` over the cheap writes would lower a host's caution on a
192
+ guess. `tasks_delete` is the case `readOnlyHint` cannot express at all: idempotent AND
193
+ destructive.
114
194
  - **Read-only mode is a convenience, not a boundary.** It removes tools from this server; the key
115
- still carries whatever scope it was minted with. Mint a `read`-scoped key for the boundary.
116
- - **A large result is truncated with a note that says so**, names how much was dropped, and points
117
- at the `limit` / `cursor` / `offset` parameters that would have avoided it. The truncated tail is
118
- not valid JSON and the note says that first, because a model that starts reading at the top will
119
- otherwise summarise half a document as though it were whole.
120
- - **Spending tools are called out in the server's instructions**: `tasks_start`, `tasks_retry`,
121
- `jobs_create` each begin a real agent run, and `notifications_act` can merge a pull request.
195
+ still carries whatever scope it was minted with. Mint a `read`-scoped key for the boundary. The
196
+ same goes for the two per-tool filters.
197
+ - **A result that does not fit is REFUSED, not truncated.** The message names how many characters
198
+ there were, the limit, and the way out from either side: `limit` / `cursor` on the list endpoints,
199
+ `offset` on the debug text reads, or a bigger `CAT_FACTORY_MCP_MAX_RESULT_CHARS`. Half an object
200
+ cannot satisfy the output schema it was cut out of, and a truncated prefix spends the whole cap on
201
+ a document whose own note tells the model not to read it.
202
+ - **Results are compact JSON.** Two-space indentation reads better to a human than to a model and
203
+ costs roughly a third of every result in whitespace; a host that wants it pretty can re-print the
204
+ structured content it also gets.
205
+ - **Spending tools are called out in the server's instructions**, derived from those same
206
+ annotations rather than restated in prose, so a tool withheld on this server is not named as one to
207
+ be careful with.
122
208
 
123
209
  ## Development
124
210
 
@@ -127,8 +213,17 @@ pnpm --filter @cat-factory/mcp-server build
127
213
  pnpm --filter @cat-factory/mcp-server test:run
128
214
  pnpm gen:sdk # regenerate the tool table after a contracts change
129
215
  pnpm check:sdk # the CI drift guard
216
+ pnpm check:publish # after a build: the empty-shell / publint / attw guard
130
217
  ```
131
218
 
132
- The end-to-end tests drive a **real** MCP client over an in-memory transport against a real SDK
133
- client whose `fetch` is stubbed, so a tool that lists but cannot be called fails a test rather than
134
- shipping.
219
+ Three layers of coverage, and the split is about what each CAN see:
220
+
221
+ - **Unit tests** (`test/`) drive a **real** MCP client over an in-memory transport against a real SDK
222
+ client whose `fetch` is stubbed, so a tool that lists but cannot be called fails a test rather than
223
+ shipping. `test/stdio.test.ts` covers the executable's rules (connect before announcing, every
224
+ human-readable byte off stdout, refuse rather than start) without spawning anything.
225
+ - **The MCP phase of `backend/internal/sdk-smoketest`** spawns the built `dist/bin.js` as a real
226
+ process against a real backend. It is the only thing that can see the published output schemas
227
+ DISAGREE with what the deployment actually answers, because the client validates them.
228
+ - **`pnpm check:publish`** covers the shape this package is most exposed to: one `bin` entry pointing
229
+ at a gitignored `dist`, which is exactly how two other packages once reached npm as empty shells.
package/dist/bin.js CHANGED
@@ -1,26 +1,18 @@
1
1
  #!/usr/bin/env node
2
- import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
- import { ENV_VARS, optionsFromEnv } from './config.js';
4
- import { createCatFactoryMcpServer } from './server.js';
2
+ import { bootStdioServer, startupFailureMessage } from './stdio.js';
5
3
  // The `cat-factory-mcp` executable: the stdio server an MCP host spawns.
6
4
  //
7
- // One rule governs everything in this file: STDOUT IS THE PROTOCOL. A stdio MCP server speaks
8
- // newline-delimited JSON-RPC over stdout, so a stray `console.log` a banner, a warning, a
9
- // debug line corrupts the stream and the host reports a server that connected and then broke.
10
- // Every human-readable byte this process writes goes to stderr, which hosts capture as logs.
11
- async function main() {
12
- const options = optionsFromEnv(process.env);
13
- const { server, tools } = createCatFactoryMcpServer(options);
14
- await server.connect(new StdioServerTransport());
15
- process.stderr.write(`cat-factory MCP server ready: ${tools.length} tools against ${options.baseUrl}\n`);
16
- }
17
- main().catch((error) => {
18
- const message = error instanceof Error ? error.message : String(error);
19
- process.stderr.write(`cat-factory MCP server failed to start: ${message}\n` +
20
- `Configure it with ${ENV_VARS.baseUrl} and ${ENV_VARS.apiKey}.\n`);
5
+ // Deliberately nothing but the process wiring: the environment, the stream every human-readable
6
+ // byte goes to (stderr, NEVER stdout, which carries the protocol), and the exit code. Everything it
7
+ // decides lives in `stdio.ts`, where a test can drive it.
8
+ bootStdioServer({
9
+ env: process.env,
10
+ log: (line) => process.stderr.write(line),
11
+ }).catch((error) => {
12
+ process.stderr.write(startupFailureMessage(error));
21
13
  // Refusing to start is the point: a server that comes up without credentials would be listed by
22
- // the host as connected and would then fail every call, which costs a model several turns to
23
- // work out and reads to a user as the platform being broken.
14
+ // the host as connected and would then fail every call, which costs a model several turns to work
15
+ // out and reads to a user as the platform being broken.
24
16
  process.exit(1);
25
17
  });
26
18
  //# sourceMappingURL=bin.js.map
package/dist/bin.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAA;AAChF,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AACtD,OAAO,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAA;AAEvD,yEAAyE;AACzE,EAAE;AACF,8FAA8F;AAC9F,4FAA4F;AAC5F,gGAAgG;AAChG,6FAA6F;AAE7F,KAAK,UAAU,IAAI;IACjB,MAAM,OAAO,GAAG,cAAc,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IAC3C,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,yBAAyB,CAAC,OAAO,CAAC,CAAA;IAC5D,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,oBAAoB,EAAE,CAAC,CAAA;IAChD,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,iCAAiC,KAAK,CAAC,MAAM,kBAAkB,OAAO,CAAC,OAAO,IAAI,CACnF,CAAA;AACH,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC9B,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IACtE,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,2CAA2C,OAAO,IAAI;QACpD,qBAAqB,QAAQ,CAAC,OAAO,QAAQ,QAAQ,CAAC,MAAM,KAAK,CACpE,CAAA;IACD,gGAAgG;IAChG,6FAA6F;IAC7F,6DAA6D;IAC7D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACjB,CAAC,CAAC,CAAA"}
1
+ {"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAEnE,yEAAyE;AACzE,EAAE;AACF,gGAAgG;AAChG,oGAAoG;AACpG,0DAA0D;AAE1D,eAAe,CAAC;IACd,GAAG,EAAE,OAAO,CAAC,GAAG;IAChB,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC;CAC1C,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;IAC1B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,qBAAqB,CAAC,KAAK,CAAC,CAAC,CAAA;IAClD,gGAAgG;IAChG,kGAAkG;IAClG,wDAAwD;IACxD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACjB,CAAC,CAAC,CAAA"}
package/dist/config.d.ts CHANGED
@@ -11,6 +11,23 @@ export interface CatFactoryMcpOptions {
11
11
  * silently exposed nothing would look exactly like a working server with a quiet model.
12
12
  */
13
13
  groups?: readonly string[];
14
+ /**
15
+ * Expose only these tools, by name (`tasks_get`, `decisions_list`, …). Absent or empty ⇒ every
16
+ * tool the other filters leave.
17
+ *
18
+ * Beside the group filter rather than instead of it: a group is the unit an operator thinks in
19
+ * ("no debug tools here"), and a tool is the unit a REFUSAL needs ("everything but the one that
20
+ * merges"). With only the coarse filter, withholding `notifications_act` cost the whole inbox.
21
+ */
22
+ tools?: readonly string[];
23
+ /**
24
+ * Withhold these tools by name, after every other filter.
25
+ *
26
+ * The deny half is the one that carries the weight: an allow-list has to be re-edited every time
27
+ * `/api/v1` grows, and a forgotten edit silently withholds a new capability, where a deny-list
28
+ * keeps admitting new tools and goes on refusing exactly what was named.
29
+ */
30
+ excludeTools?: readonly string[];
14
31
  /**
15
32
  * Expose only the tools that change nothing (the GETs). For an agent that should be able to
16
33
  * READ a deployment and never act on it.
@@ -39,12 +56,19 @@ export interface CatFactoryMcpOptions {
39
56
  export declare const ENV_VARS: {
40
57
  readonly baseUrl: 'CAT_FACTORY_BASE_URL';
41
58
  readonly apiKey: 'CAT_FACTORY_API_KEY';
59
+ readonly apiKeyFile: 'CAT_FACTORY_API_KEY_FILE';
42
60
  readonly groups: 'CAT_FACTORY_MCP_GROUPS';
61
+ readonly tools: 'CAT_FACTORY_MCP_TOOLS';
62
+ readonly excludeTools: 'CAT_FACTORY_MCP_EXCLUDE_TOOLS';
43
63
  readonly readOnly: 'CAT_FACTORY_MCP_READ_ONLY';
44
64
  readonly maxResultChars: 'CAT_FACTORY_MCP_MAX_RESULT_CHARS';
45
65
  readonly timeoutMs: 'CAT_FACTORY_MCP_TIMEOUT_MS';
46
66
  readonly maxRetries: 'CAT_FACTORY_MCP_MAX_RETRIES';
47
67
  };
68
+ /** How `optionsFromEnv` reaches the filesystem, injected so a test needs no temp file. */
69
+ export interface EnvReadDeps {
70
+ readSecretFile?: (path: string) => string;
71
+ }
48
72
  /**
49
73
  * Read the options an MCP host can supply: environment variables, because that is what every host
50
74
  * config format (Claude Desktop, an IDE, a CI runner) can set on a stdio server it spawns.
@@ -53,7 +77,7 @@ export declare const ENV_VARS: {
53
77
  * lists 36 tools and then fails every one of them is the worst of both worlds: the host reports it
54
78
  * as connected, and the model spends turns discovering that nothing works.
55
79
  */
56
- export declare function optionsFromEnv(env: Record<string, string | undefined>): CatFactoryMcpOptions;
80
+ export declare function optionsFromEnv(env: Record<string, string | undefined>, deps?: EnvReadDeps): CatFactoryMcpOptions;
57
81
  /**
58
82
  * What a server exposes, and what it withheld.
59
83
  *
@@ -70,6 +94,10 @@ export interface ToolSelection {
70
94
  filteredGroups: string[];
71
95
  /** Whether the write tools were withheld by `readOnly`. */
72
96
  writeToolsHidden: boolean;
97
+ /** Tools the operator withheld BY NAME. Empty when no deny-list was applied. */
98
+ deniedTools: string[];
99
+ /** Whether an allow-list narrowed the server to an explicitly chosen set of tools. */
100
+ toolsAllowListed: boolean;
73
101
  }
74
102
  /** The tools a set of options exposes, and what was filtered out. */
75
103
  export declare function selectTools(tools: readonly CatFactoryTool[], options: CatFactoryMcpOptions): ToolSelection;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2B,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAA;AASnF,iDAAiD;AACjD,MAAM,WAAW,oBAAoB;IACnC,uEAAuE;IACvE,OAAO,EAAE,MAAM,CAAA;IACf,6FAA6F;IAC7F,MAAM,EAAE,MAAM,CAAA;IACd;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC1B;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,iFAAiF;IACjF,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gGAAgG;IAChG,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;CAChC;AAED,wDAAwD;AACxD,eAAO,MAAM,QAAQ;aACnB,OAAO,EAAE,sBAAsB;aAC/B,MAAM,EAAE,qBAAqB;aAC7B,MAAM,EAAE,wBAAwB;aAChC,QAAQ,EAAE,2BAA2B;aACrC,cAAc,EAAE,kCAAkC;aAClD,SAAS,EAAE,4BAA4B;aACvC,UAAU,EAAE,6BAA6B;CACjC,CAAA;AAEV;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,oBAAoB,CAwB5F;AAsBD;;;;;;;;;GASG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,cAAc,EAAE,CAAA;IACzB,gFAAgF;IAChF,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,2DAA2D;IAC3D,gBAAgB,EAAE,OAAO,CAAA;CAC1B;AAED,qEAAqE;AACrE,wBAAgB,WAAW,CACzB,KAAK,EAAE,SAAS,cAAc,EAAE,EAChC,OAAO,EAAE,oBAAoB,GAC5B,aAAa,CAoBf"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AACA,OAAO,EAA2B,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAA;AASnF,iDAAiD;AACjD,MAAM,WAAW,oBAAoB;IACnC,uEAAuE;IACvE,OAAO,EAAE,MAAM,CAAA;IACf,6FAA6F;IAC7F,MAAM,EAAE,MAAM,CAAA;IACd;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAC1B;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACzB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,iFAAiF;IACjF,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,sEAAsE;IACtE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gGAAgG;IAChG,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;CAChC;AAED,wDAAwD;AACxD,eAAO,MAAM,QAAQ;aACnB,OAAO,EAAE,sBAAsB;aAC/B,MAAM,EAAE,qBAAqB;aAC7B,UAAU,EAAE,0BAA0B;aACtC,MAAM,EAAE,wBAAwB;aAChC,KAAK,EAAE,uBAAuB;aAC9B,YAAY,EAAE,+BAA+B;aAC7C,QAAQ,EAAE,2BAA2B;aACrC,cAAc,EAAE,kCAAkC;aAClD,SAAS,EAAE,4BAA4B;aACvC,UAAU,EAAE,6BAA6B;CACjC,CAAA;AAEV,0FAA0F;AAC1F,MAAM,WAAW,WAAW;IAC1B,cAAc,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;CAC1C;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,EACvC,IAAI,GAAE,WAAgB,GACrB,oBAAoB,CAwBtB;AA8ED;;;;;;;;;GASG;AACH,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,cAAc,EAAE,CAAA;IACzB,gFAAgF;IAChF,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,2DAA2D;IAC3D,gBAAgB,EAAE,OAAO,CAAA;IACzB,gFAAgF;IAChF,WAAW,EAAE,MAAM,EAAE,CAAA;IACrB,sFAAsF;IACtF,gBAAgB,EAAE,OAAO,CAAA;CAC1B;AAED,qEAAqE;AACrE,wBAAgB,WAAW,CACzB,KAAK,EAAE,SAAS,cAAc,EAAE,EAChC,OAAO,EAAE,oBAAoB,GAC5B,aAAa,CA2Cf"}
package/dist/config.js CHANGED
@@ -1,9 +1,13 @@
1
+ import { readFileSync } from 'node:fs';
1
2
  import { CAT_FACTORY_TOOL_GROUPS } from './tools.generated.js';
2
3
  /** The environment variables `optionsFromEnv` reads. */
3
4
  export const ENV_VARS = {
4
5
  baseUrl: 'CAT_FACTORY_BASE_URL',
5
6
  apiKey: 'CAT_FACTORY_API_KEY',
7
+ apiKeyFile: 'CAT_FACTORY_API_KEY_FILE',
6
8
  groups: 'CAT_FACTORY_MCP_GROUPS',
9
+ tools: 'CAT_FACTORY_MCP_TOOLS',
10
+ excludeTools: 'CAT_FACTORY_MCP_EXCLUDE_TOOLS',
7
11
  readOnly: 'CAT_FACTORY_MCP_READ_ONLY',
8
12
  maxResultChars: 'CAT_FACTORY_MCP_MAX_RESULT_CHARS',
9
13
  timeoutMs: 'CAT_FACTORY_MCP_TIMEOUT_MS',
@@ -17,36 +21,86 @@ export const ENV_VARS = {
17
21
  * lists 36 tools and then fails every one of them is the worst of both worlds: the host reports it
18
22
  * as connected, and the model spends turns discovering that nothing works.
19
23
  */
20
- export function optionsFromEnv(env) {
24
+ export function optionsFromEnv(env, deps = {}) {
21
25
  const baseUrl = env[ENV_VARS.baseUrl]?.trim();
22
- const apiKey = env[ENV_VARS.apiKey]?.trim();
23
26
  if (!baseUrl)
24
27
  throw new Error(`${ENV_VARS.baseUrl} is required (the deployment's origin).`);
25
- if (!apiKey)
26
- throw new Error(`${ENV_VARS.apiKey} is required (a public-API key).`);
27
- const groups = env[ENV_VARS.groups]
28
- ?.split(',')
29
- .map((group) => group.trim())
30
- .filter(Boolean);
28
+ const apiKey = readApiKey(env, deps);
31
29
  // Read each ceiling ONCE. Every one of these is optional and must stay ABSENT rather than
32
30
  // become `undefined`, because the server spreads them onto the SDK's own options and an
33
31
  // explicit `undefined` is not the same as not passing the field.
32
+ const groups = list(env[ENV_VARS.groups]);
33
+ const tools = list(env[ENV_VARS.tools]);
34
+ const excludeTools = list(env[ENV_VARS.excludeTools]);
34
35
  const maxResultChars = numeric(env[ENV_VARS.maxResultChars], ENV_VARS.maxResultChars);
35
36
  const timeoutMs = numeric(env[ENV_VARS.timeoutMs], ENV_VARS.timeoutMs);
36
37
  const maxRetries = numeric(env[ENV_VARS.maxRetries], ENV_VARS.maxRetries);
37
38
  return {
38
39
  baseUrl,
39
40
  apiKey,
40
- ...(groups && groups.length > 0 ? { groups } : {}),
41
+ ...(groups ? { groups } : {}),
42
+ ...(tools ? { tools } : {}),
43
+ ...(excludeTools ? { excludeTools } : {}),
41
44
  ...(isTruthy(env[ENV_VARS.readOnly]) ? { readOnly: true } : {}),
42
45
  ...(maxResultChars !== undefined ? { maxResultChars } : {}),
43
46
  ...(timeoutMs !== undefined ? { timeoutMs } : {}),
44
47
  ...(maxRetries !== undefined ? { maxRetries } : {}),
45
48
  };
46
49
  }
50
+ /**
51
+ * The API key, from the variable or from the FILE the variable names.
52
+ *
53
+ * The file exists because a stdio server's environment is a host's config file: on every host that
54
+ * matters that means a long-lived credential sitting in plaintext in a user's home directory, read
55
+ * by every process that can read the file and copied into every backup and every screen share of
56
+ * that config. A path is not a secret, so pointing at one keeps the key in a file the operator can
57
+ * lock down (`chmod 600`, a mounted secret, a secrets-manager sidecar's drop) and out of the config
58
+ * that gets shared.
59
+ *
60
+ * Declaring BOTH is refused rather than resolved by precedence. Two live sources for one credential
61
+ * means a rotation can land on the one that is not being read, and the deployment goes on working
62
+ * with the old key until it is revoked, which is exactly the moment nobody is looking.
63
+ */
64
+ function readApiKey(env, deps) {
65
+ const inline = env[ENV_VARS.apiKey]?.trim();
66
+ const path = env[ENV_VARS.apiKeyFile]?.trim();
67
+ if (inline && path) {
68
+ throw new Error(`${ENV_VARS.apiKey} and ${ENV_VARS.apiKeyFile} are both set. Pick one: two sources for one ` +
69
+ 'credential means a rotation can land on the half nobody reads.');
70
+ }
71
+ if (inline)
72
+ return inline;
73
+ if (!path) {
74
+ throw new Error(`${ENV_VARS.apiKey} is required (a public-API key), or ${ENV_VARS.apiKeyFile} naming a file ` +
75
+ 'that holds one.');
76
+ }
77
+ const read = deps.readSecretFile ?? ((target) => readFileSync(target, 'utf8'));
78
+ let contents;
79
+ try {
80
+ contents = read(path);
81
+ }
82
+ catch (error) {
83
+ // The PATH is named and the cause is passed through; the contents never are, on any branch of
84
+ // this function, because this message goes to a host's log.
85
+ throw new Error(`${ENV_VARS.apiKeyFile} points at ${path}, which could not be read: ` +
86
+ `${error instanceof Error ? error.message : String(error)}`);
87
+ }
88
+ const key = contents.trim();
89
+ if (!key)
90
+ throw new Error(`${ENV_VARS.apiKeyFile} points at ${path}, which is empty.`);
91
+ return key;
92
+ }
47
93
  function isTruthy(value) {
48
94
  return value === '1' || value?.toLowerCase() === 'true';
49
95
  }
96
+ /** A comma-separated list, or undefined when the variable is unset or lists nothing. */
97
+ function list(value) {
98
+ const entries = value
99
+ ?.split(',')
100
+ .map((entry) => entry.trim())
101
+ .filter(Boolean);
102
+ return entries && entries.length > 0 ? entries : undefined;
103
+ }
50
104
  /**
51
105
  * A numeric env value, or undefined when unset.
52
106
  *
@@ -73,12 +127,50 @@ export function selectTools(tools, options) {
73
127
  throw new Error(`Unknown tool group(s): ${unknown.join(', ')}. Known groups: ${[...known].join(', ')}.`);
74
128
  }
75
129
  }
76
- const exposed = tools.filter((tool) => (!requested || requested.has(tool.group)) && (!options.readOnly || tool.readOnly));
130
+ // Both name filters are checked against the WHOLE table rather than against what the group and
131
+ // read-only filters have already left, so a name is judged on whether it exists at all. Checking
132
+ // against the survivors would turn a redundant deny (a tool in a group that is off anyway) into a
133
+ // startup failure, and redundancy in a safety list is not a mistake worth refusing to boot over.
134
+ const allowed = namedTools(tools, options.tools, ENV_VARS.tools);
135
+ const denied = namedTools(tools, options.excludeTools, ENV_VARS.excludeTools);
136
+ const exposed = tools.filter((tool) => (!requested || requested.has(tool.group)) &&
137
+ (!options.readOnly || tool.readOnly) &&
138
+ (!allowed || allowed.has(tool.name)) &&
139
+ !denied?.has(tool.name));
140
+ // Filters that combine to expose NOTHING are refused for the same reason an unknown group name
141
+ // is: the host reports the server as connected either way, and a model with no tools looks
142
+ // exactly like a model that has decided not to use any.
143
+ if (exposed.length === 0) {
144
+ throw new Error('The configured filters expose no tools at all: ' +
145
+ `groups=${options.groups?.join(',') ?? '(all)'}, readOnly=${options.readOnly === true}, ` +
146
+ `tools=${options.tools?.join(',') ?? '(all)'}, ` +
147
+ `excludeTools=${options.excludeTools?.join(',') ?? '(none)'}.`);
148
+ }
77
149
  const filteredGroups = [...known].filter((group) => requested !== null && !requested.has(group));
78
150
  return {
79
151
  exposed,
80
152
  filteredGroups,
81
153
  writeToolsHidden: options.readOnly === true,
154
+ deniedTools: denied ? [...denied].sort() : [],
155
+ toolsAllowListed: allowed !== null,
82
156
  };
83
157
  }
158
+ /**
159
+ * A validated set of tool names, or null when the filter is unset.
160
+ *
161
+ * An unknown name throws, naming the variable that carried it. A deny-list is the one filter whose
162
+ * typo is silently DANGEROUS: `notifications_action` withholds nothing, and the tool the operator
163
+ * meant to keep away from a model goes on being served.
164
+ */
165
+ function namedTools(tools, names, variable) {
166
+ if (!names?.length)
167
+ return null;
168
+ const known = new Set(tools.map((tool) => tool.name));
169
+ const unknown = names.filter((name) => !known.has(name));
170
+ if (unknown.length > 0) {
171
+ throw new Error(`${variable} names tool(s) this server does not have: ${unknown.join(', ')}. ` +
172
+ `A tool is named <group>_<method>; the known ones are: ${[...known].sort().join(', ')}.`);
173
+ }
174
+ return new Set(names);
175
+ }
84
176
  //# sourceMappingURL=config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { createCatFactoryMcpServer, type CatFactoryMcpServer, MCP_SERVER_NAME, MCP_SERVER_VERSION, } from './server.ts';
2
- export { type CatFactoryMcpOptions, ENV_VARS, optionsFromEnv, selectTools } from './config.ts';
2
+ export { type CatFactoryMcpOptions, ENV_VARS, type EnvReadDeps, optionsFromEnv, selectTools, type ToolSelection, } from './config.ts';
3
3
  export { buildInstructions } from './instructions.ts';
4
4
  export { DEFAULT_MAX_RESULT_CHARS, renderError, renderResult, type ToolResult } from './result.ts';
5
+ export { bootStdioServer, type StdioBootDeps, type StdioBootResult, startupFailureMessage, } from './stdio.ts';
5
6
  export { CAT_FACTORY_OMITTED_OPERATIONS, CAT_FACTORY_TOOL_GROUPS, CAT_FACTORY_TOOLS, type CatFactoryOmittedOperation, type CatFactoryTool, } from './tools.generated.ts';
6
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,OAAO,EACL,yBAAyB,EACzB,KAAK,mBAAmB,EACxB,eAAe,EACf,kBAAkB,GACnB,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,KAAK,oBAAoB,EAAE,QAAQ,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAC9F,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AACrD,OAAO,EAAE,wBAAwB,EAAE,WAAW,EAAE,YAAY,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAClG,OAAO,EACL,8BAA8B,EAC9B,uBAAuB,EACvB,iBAAiB,EACjB,KAAK,0BAA0B,EAC/B,KAAK,cAAc,GACpB,MAAM,sBAAsB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,OAAO,EACL,yBAAyB,EACzB,KAAK,mBAAmB,EACxB,eAAe,EACf,kBAAkB,GACnB,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,KAAK,oBAAoB,EACzB,QAAQ,EACR,KAAK,WAAW,EAChB,cAAc,EACd,WAAW,EACX,KAAK,aAAa,GACnB,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AACrD,OAAO,EAAE,wBAAwB,EAAE,WAAW,EAAE,YAAY,EAAE,KAAK,UAAU,EAAE,MAAM,aAAa,CAAA;AAClG,OAAO,EACL,eAAe,EACf,KAAK,aAAa,EAClB,KAAK,eAAe,EACpB,qBAAqB,GACtB,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,8BAA8B,EAC9B,uBAAuB,EACvB,iBAAiB,EACjB,KAAK,0BAA0B,EAC/B,KAAK,cAAc,GACpB,MAAM,sBAAsB,CAAA"}
package/dist/index.js CHANGED
@@ -8,8 +8,9 @@
8
8
  // Run it as a stdio server with the `cat-factory-mcp` binary, or mount it on your own transport
9
9
  // with `createCatFactoryMcpServer`.
10
10
  export { createCatFactoryMcpServer, MCP_SERVER_NAME, MCP_SERVER_VERSION, } from './server.js';
11
- export { ENV_VARS, optionsFromEnv, selectTools } from './config.js';
11
+ export { ENV_VARS, optionsFromEnv, selectTools, } from './config.js';
12
12
  export { buildInstructions } from './instructions.js';
13
13
  export { DEFAULT_MAX_RESULT_CHARS, renderError, renderResult } from './result.js';
14
+ export { bootStdioServer, startupFailureMessage, } from './stdio.js';
14
15
  export { CAT_FACTORY_OMITTED_OPERATIONS, CAT_FACTORY_TOOL_GROUPS, CAT_FACTORY_TOOLS, } from './tools.generated.js';
15
16
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,EAAE;AACF,+FAA+F;AAC/F,mGAAmG;AACnG,8FAA8F;AAC9F,uFAAuF;AACvF,EAAE;AACF,gGAAgG;AAChG,oCAAoC;AAEpC,OAAO,EACL,yBAAyB,EAEzB,eAAe,EACf,kBAAkB,GACnB,MAAM,aAAa,CAAA;AACpB,OAAO,EAA6B,QAAQ,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAC9F,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AACrD,OAAO,EAAE,wBAAwB,EAAE,WAAW,EAAE,YAAY,EAAmB,MAAM,aAAa,CAAA;AAClG,OAAO,EACL,8BAA8B,EAC9B,uBAAuB,EACvB,iBAAiB,GAGlB,MAAM,sBAAsB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,EAAE;AACF,+FAA+F;AAC/F,mGAAmG;AACnG,8FAA8F;AAC9F,uFAAuF;AACvF,EAAE;AACF,gGAAgG;AAChG,oCAAoC;AAEpC,OAAO,EACL,yBAAyB,EAEzB,eAAe,EACf,kBAAkB,GACnB,MAAM,aAAa,CAAA;AACpB,OAAO,EAEL,QAAQ,EAER,cAAc,EACd,WAAW,GAEZ,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AACrD,OAAO,EAAE,wBAAwB,EAAE,WAAW,EAAE,YAAY,EAAmB,MAAM,aAAa,CAAA;AAClG,OAAO,EACL,eAAe,EAGf,qBAAqB,GACtB,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,8BAA8B,EAC9B,uBAAuB,EACvB,iBAAiB,GAGlB,MAAM,sBAAsB,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"instructions.d.ts","sourceRoot":"","sources":["../src/instructions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAWhD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,aAAa,GAAG,MAAM,CAwClE"}
1
+ {"version":3,"file":"instructions.d.ts","sourceRoot":"","sources":["../src/instructions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAWhD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,aAAa,GAAG,MAAM,CAkElE"}