@cat-factory/mcp-server 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Igor Savin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,134 @@
1
+ # `@cat-factory/mcp-server`
2
+
3
+ A [Model Context Protocol](https://modelcontextprotocol.io) server over the cat-factory **public
4
+ API** (`/api/v1`). It lets an MCP host — Claude Desktop, an IDE, an agent framework — plan work on a
5
+ workspace's board, start and watch runs, answer parked decisions, and read a run's telemetry.
6
+
7
+ It is a **facade**, not a client. Every tool is one call on
8
+ [`@cat-factory/sdk`](../typescript), and the tool table is generated from the same
9
+ [`docs/openapi.json`](../../docs/openapi.json) the four SDK clients are generated from.
10
+
11
+ ## Run it
12
+
13
+ ```jsonc
14
+ // An MCP host's server config
15
+ {
16
+ "mcpServers": {
17
+ "cat-factory": {
18
+ "command": "npx",
19
+ "args": ["-y", "@cat-factory/mcp-server"],
20
+ "env": {
21
+ "CAT_FACTORY_BASE_URL": "https://cat-factory.example.com",
22
+ "CAT_FACTORY_API_KEY": "cf_live_...",
23
+ },
24
+ },
25
+ },
26
+ }
27
+ ```
28
+
29
+ Mint the key from the deployment (`backend/docs/public-api.md` §Setup) and give it the **narrowest
30
+ scope that does the job**: `read ⊂ write ⊂ decide ⊂ admin`. Every tool is scoped to the key's
31
+ workspace, and the key is what actually decides what a model can do here.
32
+
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.
46
+
47
+ To mount it on your own transport instead of stdio:
48
+
49
+ ```ts
50
+ import { createCatFactoryMcpServer } from '@cat-factory/mcp-server'
51
+
52
+ const { server, tools } = createCatFactoryMcpServer({ baseUrl, apiKey })
53
+ await server.connect(myTransport)
54
+ ```
55
+
56
+ ## What "thin" means here
57
+
58
+ The facade decides three things: which tools to list, how to render one result, and how to render
59
+ one failure. Everything else — auth, retries, error classes, pagination, encoding, deadlines — is
60
+ the SDK's, so a tool behaves exactly like the same call made from code. A second implementation of
61
+ those rules would be a second place to get them subtly wrong.
62
+
63
+ Concretely:
64
+
65
+ - **The tool table is generated** (`src/tools.generated.ts`, by `pnpm gen:sdk`), so an endpoint
66
+ added to `/api/v1` becomes a tool without anyone deciding twice, and `pnpm check:sdk` fails CI on
67
+ drift. Never edit it by hand.
68
+ - **The input schema is the spec's own.** A tool's `body` schema is rendered from the same
69
+ contract the deployment validates against, so a tool cannot describe a request shape the server
70
+ would reject. An OPEN vocabulary (a closed set plus a documented escape hatch, like `taskType`)
71
+ is never narrowed to an `enum`, because that would refuse a value the server accepts.
72
+ - **Argument validation stops at the door.** Required path ids are checked so a request can be
73
+ built at all; the body goes to the deployment, whose 422 names the field and is far more useful
74
+ to a model than anything this layer could say.
75
+ - **A failure is tool content, not a protocol error.** A protocol error means the SERVER
76
+ misbehaved and hosts do not show it to the model. A 422 with `code`, `details.reason` and the
77
+ per-field `issues` is the most actionable thing this facade ever returns, so it is passed
78
+ through verbatim rather than re-worded.
79
+
80
+ ## The tool surface
81
+
82
+ One tool per exposed operation, named `<group>_<method>` to match the SDK call
83
+ (`client.tasks.create()` ⇄ `tasks_create`). The server reports the live count on startup and lists
84
+ the tools themselves over the protocol, which is the only place it cannot go stale:
85
+
86
+ | Group | What it covers |
87
+ | ----------------- | ------------------------------------------------------------------------- |
88
+ | `jobs_*` | Headless runs of a public inline pipeline against a brief. |
89
+ | `services_*` | The board's service frames. |
90
+ | `tasks_*` | A task's whole lifecycle: create, edit, start, stop, retry, read its run. |
91
+ | `pipelines_*` | Which pipelines a task can be started with. |
92
+ | `notifications_*` | The human-actionable inbox, including the merge tail. |
93
+ | `usage_*` | The billing period's metered budget position. |
94
+ | `decisions_*` | A parked run's human decisions. |
95
+ | `debug_*` | A run's recorded telemetry: LLM calls, agent context, infra logs. |
96
+
97
+ **The API's two SSE operations are deliberately absent**, and the server says so in its
98
+ instructions rather than leaving a model to conclude the platform cannot do it:
99
+ `GET /api/v1/jobs/{id}/events` and `GET /api/v1/tasks/{taskId}/events` are SSE streams, and a tool
100
+ call returns one result over no streaming channel. Poll `jobs_get` / `tasks_get_run`, or consume the
101
+ streams through an SDK. Adding a bounded "wait for the run" tool would not fix this: a run parked on
102
+ a human decision waits **indefinitely** by design, so any such tool is a timeout dressed up as an
103
+ answer.
104
+
105
+ Generation FAILS on a new streaming operation nobody has classified, so the list above cannot go
106
+ quietly stale.
107
+
108
+ ## Things a caller should know
109
+
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.
114
+ - **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.
122
+
123
+ ## Development
124
+
125
+ ```sh
126
+ pnpm --filter @cat-factory/mcp-server build
127
+ pnpm --filter @cat-factory/mcp-server test:run
128
+ pnpm gen:sdk # regenerate the tool table after a contracts change
129
+ pnpm check:sdk # the CI drift guard
130
+ ```
131
+
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.
package/dist/bin.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=bin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":""}
package/dist/bin.js ADDED
@@ -0,0 +1,26 @@
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';
5
+ // The `cat-factory-mcp` executable: the stdio server an MCP host spawns.
6
+ //
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`);
21
+ // 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.
24
+ process.exit(1);
25
+ });
26
+ //# sourceMappingURL=bin.js.map
@@ -0,0 +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"}
@@ -0,0 +1,76 @@
1
+ import { type CatFactoryTool } from './tools.generated.ts';
2
+ /** How a `CatFactoryMcpServer` is configured. */
3
+ export interface CatFactoryMcpOptions {
4
+ /** The deployment's origin, e.g. `https://cat-factory.example.com`. */
5
+ baseUrl: string;
6
+ /** A public-API key: `cf_live_<keyId>.<secret>`. Its SCOPE decides what the tools may do. */
7
+ apiKey: string;
8
+ /**
9
+ * Expose only these resource groups (`jobs`, `tasks`, `debug`, …). Absent or empty ⇒ all of
10
+ * them. An unknown group name is a startup ERROR rather than an empty selection: a typo that
11
+ * silently exposed nothing would look exactly like a working server with a quiet model.
12
+ */
13
+ groups?: readonly string[];
14
+ /**
15
+ * Expose only the tools that change nothing (the GETs). For an agent that should be able to
16
+ * READ a deployment and never act on it.
17
+ *
18
+ * This is a convenience, NOT a security boundary: the tools are gone from this server, but the
19
+ * key still carries whatever scope it was minted with, and anything else holding it can still
20
+ * write. Mint a `read`-scoped key for the boundary.
21
+ */
22
+ readOnly?: boolean;
23
+ /** Ceiling on one tool result, in characters. See `DEFAULT_MAX_RESULT_CHARS`. */
24
+ maxResultChars?: number;
25
+ /** Per-request deadline in ms, passed to the SDK; `0` disables it. */
26
+ timeoutMs?: number;
27
+ /** Retries for a retriable failure, passed to the SDK. Only idempotent requests are retried. */
28
+ maxRetries?: number;
29
+ /**
30
+ * Swap the HTTP implementation (a proxy agent, a test double), passed straight to the SDK.
31
+ *
32
+ * This is the ONLY injection point, deliberately: the facade always constructs its own client,
33
+ * so a caller cannot hand it one pointed at a different deployment than these options describe,
34
+ * or one that omits the `User-Agent` the deployment's audit trail identifies a model's calls by.
35
+ */
36
+ fetch?: typeof globalThis.fetch;
37
+ }
38
+ /** The environment variables `optionsFromEnv` reads. */
39
+ export declare const ENV_VARS: {
40
+ readonly baseUrl: 'CAT_FACTORY_BASE_URL';
41
+ readonly apiKey: 'CAT_FACTORY_API_KEY';
42
+ readonly groups: 'CAT_FACTORY_MCP_GROUPS';
43
+ readonly readOnly: 'CAT_FACTORY_MCP_READ_ONLY';
44
+ readonly maxResultChars: 'CAT_FACTORY_MCP_MAX_RESULT_CHARS';
45
+ readonly timeoutMs: 'CAT_FACTORY_MCP_TIMEOUT_MS';
46
+ readonly maxRetries: 'CAT_FACTORY_MCP_MAX_RETRIES';
47
+ };
48
+ /**
49
+ * Read the options an MCP host can supply: environment variables, because that is what every host
50
+ * config format (Claude Desktop, an IDE, a CI runner) can set on a stdio server it spawns.
51
+ *
52
+ * Missing credentials THROW here rather than at the first tool call. An MCP server that starts,
53
+ * lists 36 tools and then fails every one of them is the worst of both worlds: the host reports it
54
+ * as connected, and the model spends turns discovering that nothing works.
55
+ */
56
+ export declare function optionsFromEnv(env: Record<string, string | undefined>): CatFactoryMcpOptions;
57
+ /**
58
+ * What a server exposes, and what it withheld.
59
+ *
60
+ * The withheld halves are carried rather than discarded because the server states them in its
61
+ * instructions: a model that can see `tasks_*` but not `debug_*` should be told the debug tools
62
+ * were switched off HERE, or it will report the deployment as not supporting them. That is why
63
+ * this is ONE value rather than a tool list plus a second look at the options. The instructions
64
+ * are written from what the selection actually did, so a filter cannot be applied and left
65
+ * unmentioned.
66
+ */
67
+ export interface ToolSelection {
68
+ exposed: CatFactoryTool[];
69
+ /** Groups the operator switched off. Empty when no group filter was applied. */
70
+ filteredGroups: string[];
71
+ /** Whether the write tools were withheld by `readOnly`. */
72
+ writeToolsHidden: boolean;
73
+ }
74
+ /** The tools a set of options exposes, and what was filtered out. */
75
+ export declare function selectTools(tools: readonly CatFactoryTool[], options: CatFactoryMcpOptions): ToolSelection;
76
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +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"}
package/dist/config.js ADDED
@@ -0,0 +1,84 @@
1
+ import { CAT_FACTORY_TOOL_GROUPS } from './tools.generated.js';
2
+ /** The environment variables `optionsFromEnv` reads. */
3
+ export const ENV_VARS = {
4
+ baseUrl: 'CAT_FACTORY_BASE_URL',
5
+ apiKey: 'CAT_FACTORY_API_KEY',
6
+ groups: 'CAT_FACTORY_MCP_GROUPS',
7
+ readOnly: 'CAT_FACTORY_MCP_READ_ONLY',
8
+ maxResultChars: 'CAT_FACTORY_MCP_MAX_RESULT_CHARS',
9
+ timeoutMs: 'CAT_FACTORY_MCP_TIMEOUT_MS',
10
+ maxRetries: 'CAT_FACTORY_MCP_MAX_RETRIES',
11
+ };
12
+ /**
13
+ * Read the options an MCP host can supply: environment variables, because that is what every host
14
+ * config format (Claude Desktop, an IDE, a CI runner) can set on a stdio server it spawns.
15
+ *
16
+ * Missing credentials THROW here rather than at the first tool call. An MCP server that starts,
17
+ * lists 36 tools and then fails every one of them is the worst of both worlds: the host reports it
18
+ * as connected, and the model spends turns discovering that nothing works.
19
+ */
20
+ export function optionsFromEnv(env) {
21
+ const baseUrl = env[ENV_VARS.baseUrl]?.trim();
22
+ const apiKey = env[ENV_VARS.apiKey]?.trim();
23
+ if (!baseUrl)
24
+ 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);
31
+ // Read each ceiling ONCE. Every one of these is optional and must stay ABSENT rather than
32
+ // become `undefined`, because the server spreads them onto the SDK's own options and an
33
+ // explicit `undefined` is not the same as not passing the field.
34
+ const maxResultChars = numeric(env[ENV_VARS.maxResultChars], ENV_VARS.maxResultChars);
35
+ const timeoutMs = numeric(env[ENV_VARS.timeoutMs], ENV_VARS.timeoutMs);
36
+ const maxRetries = numeric(env[ENV_VARS.maxRetries], ENV_VARS.maxRetries);
37
+ return {
38
+ baseUrl,
39
+ apiKey,
40
+ ...(groups && groups.length > 0 ? { groups } : {}),
41
+ ...(isTruthy(env[ENV_VARS.readOnly]) ? { readOnly: true } : {}),
42
+ ...(maxResultChars !== undefined ? { maxResultChars } : {}),
43
+ ...(timeoutMs !== undefined ? { timeoutMs } : {}),
44
+ ...(maxRetries !== undefined ? { maxRetries } : {}),
45
+ };
46
+ }
47
+ function isTruthy(value) {
48
+ return value === '1' || value?.toLowerCase() === 'true';
49
+ }
50
+ /**
51
+ * A numeric env value, or undefined when unset.
52
+ *
53
+ * A NON-numeric value throws rather than falling back to the default. Everything this configures
54
+ * is a ceiling, and a mistyped ceiling that silently reverts to the built-in one is a limit an
55
+ * operator believes is in force and is not.
56
+ */
57
+ function numeric(value, name) {
58
+ if (value === undefined || value.trim() === '')
59
+ return undefined;
60
+ const parsed = Number(value);
61
+ if (!Number.isFinite(parsed) || parsed < 0) {
62
+ throw new Error(`${name} must be a non-negative number; got ${JSON.stringify(value)}.`);
63
+ }
64
+ return parsed;
65
+ }
66
+ /** The tools a set of options exposes, and what was filtered out. */
67
+ export function selectTools(tools, options) {
68
+ const known = new Set(Object.keys(CAT_FACTORY_TOOL_GROUPS));
69
+ const requested = options.groups?.length ? new Set(options.groups) : null;
70
+ if (requested) {
71
+ const unknown = [...requested].filter((group) => !known.has(group));
72
+ if (unknown.length > 0) {
73
+ throw new Error(`Unknown tool group(s): ${unknown.join(', ')}. Known groups: ${[...known].join(', ')}.`);
74
+ }
75
+ }
76
+ const exposed = tools.filter((tool) => (!requested || requested.has(tool.group)) && (!options.readOnly || tool.readOnly));
77
+ const filteredGroups = [...known].filter((group) => requested !== null && !requested.has(group));
78
+ return {
79
+ exposed,
80
+ filteredGroups,
81
+ writeToolsHidden: options.readOnly === true,
82
+ };
83
+ }
84
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
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';
3
+ export { buildInstructions } from './instructions.ts';
4
+ export { DEFAULT_MAX_RESULT_CHARS, renderError, renderResult, type ToolResult } from './result.ts';
5
+ export { CAT_FACTORY_OMITTED_OPERATIONS, CAT_FACTORY_TOOL_GROUPS, CAT_FACTORY_TOOLS, type CatFactoryOmittedOperation, type CatFactoryTool, } from './tools.generated.ts';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ // `@cat-factory/mcp-server` — a Model Context Protocol facade over the cat-factory public API.
2
+ //
3
+ // The tool table is GENERATED from `docs/openapi.json` (the same spec the four SDK clients are
4
+ // generated from), and every tool is one call on the hand-written `@cat-factory/sdk` transport. So
5
+ // the facade cannot drift from the surface it exposes, and it re-implements none of the SDK's
6
+ // behaviour — retries, auth, error classes, pagination and encoding are all the SDK's.
7
+ //
8
+ // Run it as a stdio server with the `cat-factory-mcp` binary, or mount it on your own transport
9
+ // with `createCatFactoryMcpServer`.
10
+ export { createCatFactoryMcpServer, MCP_SERVER_NAME, MCP_SERVER_VERSION, } from './server.js';
11
+ export { ENV_VARS, optionsFromEnv, selectTools } from './config.js';
12
+ export { buildInstructions } from './instructions.js';
13
+ export { DEFAULT_MAX_RESULT_CHARS, renderError, renderResult } from './result.js';
14
+ export { CAT_FACTORY_OMITTED_OPERATIONS, CAT_FACTORY_TOOL_GROUPS, CAT_FACTORY_TOOLS, } from './tools.generated.js';
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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"}
@@ -0,0 +1,10 @@
1
+ import type { ToolSelection } from './config.ts';
2
+ /**
3
+ * The server's instructions for a tool selection.
4
+ *
5
+ * Takes the SELECTION rather than the options it came from: what a model needs told is what was
6
+ * actually withheld, and reading that back off the options would be a second derivation of it,
7
+ * free to disagree with the first.
8
+ */
9
+ export declare function buildInstructions(selection: ToolSelection): string;
10
+ //# sourceMappingURL=instructions.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,49 @@
1
+ import { CAT_FACTORY_OMITTED_OPERATIONS, CAT_FACTORY_TOOL_GROUPS } from './tools.generated.js';
2
+ // The server's `instructions`: the one piece of prose a model reads before it reads any tool.
3
+ //
4
+ // It exists to answer the three questions the tool list cannot. What is this deployment (a model
5
+ // asked to "check the failing run" has no idea it is talking to an agent platform). What is
6
+ // deliberately NOT here, and what to use instead (an absent capability reads as an unsupported
7
+ // one, and the model then tells its user the platform cannot do it). And which of the things it
8
+ // can see cost real money or real time — because on this surface, some do.
9
+ /**
10
+ * The server's instructions for a tool selection.
11
+ *
12
+ * Takes the SELECTION rather than the options it came from: what a model needs told is what was
13
+ * actually withheld, and reading that back off the options would be a second derivation of it,
14
+ * free to disagree with the first.
15
+ */
16
+ export function buildInstructions(selection) {
17
+ const { exposed, filteredGroups, writeToolsHidden } = selection;
18
+ const groups = [...new Set(exposed.map((tool) => tool.group))];
19
+ const sections = [
20
+ "cat-factory runs coding agents against a team's real repositories: a board of services and " +
21
+ 'tasks, each task run through a pipeline of agent steps that opens and merges pull requests. ' +
22
+ 'These tools are the public API of ONE workspace, the one the configured key belongs to.',
23
+ `Available tool groups:\n${groups
24
+ .map((group) => `- ${group}: ${CAT_FACTORY_TOOL_GROUPS[group]}`)
25
+ .join('\n')}`,
26
+ 'Two things here cost real money and real time, so confirm with the user before calling them: ' +
27
+ '`tasks_start` / `tasks_retry` and `jobs_create` each begin an agent run against a real ' +
28
+ 'repository, and `notifications_act` can merge a pull request. Everything else is cheap.',
29
+ 'A run PARKS on a human decision and then waits indefinitely by design; it is not stuck. Read ' +
30
+ 'the park with `decisions_list` and answer it with the other `decisions_*` tools, or leave ' +
31
+ 'it for a person.',
32
+ 'Results are JSON. Lists are keyset-paginated: pass the `cursor` a page returns to get the ' +
33
+ 'next one, and stop when it comes back null (an empty page with a cursor is normal).',
34
+ ];
35
+ if (writeToolsHidden) {
36
+ sections.push('This server was started READ-ONLY: only the tools that change nothing are exposed. The ' +
37
+ 'deployment supports creating and running tasks; you cannot do it from here.');
38
+ }
39
+ if (filteredGroups.length > 0) {
40
+ sections.push(`The operator switched off these tool groups on THIS server: ${filteredGroups.join(', ')}. ` +
41
+ 'The deployment still supports them; they are not reachable from here.');
42
+ }
43
+ // The omissions are stated in the same voice as the filters above, and for the same reason: a
44
+ // model that cannot find a way to watch a run live should learn that the platform streams and
45
+ // that a tool call is the wrong shape for it, rather than concluding the platform does not.
46
+ sections.push(`Not available as tools:\n${CAT_FACTORY_OMITTED_OPERATIONS.map((omitted) => `- ${omitted.route}: ${omitted.reason}`).join('\n')}`);
47
+ return sections.join('\n\n');
48
+ }
49
+ //# sourceMappingURL=instructions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instructions.js","sourceRoot":"","sources":["../src/instructions.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,8BAA8B,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAA;AAE9F,8FAA8F;AAC9F,EAAE;AACF,iGAAiG;AACjG,4FAA4F;AAC5F,+FAA+F;AAC/F,gGAAgG;AAChG,2EAA2E;AAE3E;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,SAAwB;IACxD,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,GAAG,SAAS,CAAA;IAC/D,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IAC9D,MAAM,QAAQ,GAAa;QACzB,6FAA6F;YAC3F,8FAA8F;YAC9F,yFAAyF;QAC3F,2BAA2B,MAAM;aAC9B,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,KAAK,uBAAuB,CAAC,KAAK,CAAC,EAAE,CAAC;aAC/D,IAAI,CAAC,IAAI,CAAC,EAAE;QACf,+FAA+F;YAC7F,yFAAyF;YACzF,yFAAyF;QAC3F,+FAA+F;YAC7F,4FAA4F;YAC5F,kBAAkB;QACpB,4FAA4F;YAC1F,qFAAqF;KACxF,CAAA;IACD,IAAI,gBAAgB,EAAE,CAAC;QACrB,QAAQ,CAAC,IAAI,CACX,yFAAyF;YACvF,6EAA6E,CAChF,CAAA;IACH,CAAC;IACD,IAAI,cAAc,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC9B,QAAQ,CAAC,IAAI,CACX,+DAA+D,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;YAC1F,uEAAuE,CAC1E,CAAA;IACH,CAAC;IACD,8FAA8F;IAC9F,8FAA8F;IAC9F,4FAA4F;IAC5F,QAAQ,CAAC,IAAI,CACX,4BAA4B,8BAA8B,CAAC,GAAG,CAC5D,CAAC,OAAO,EAAE,EAAE,CAAC,KAAK,OAAO,CAAC,KAAK,KAAK,OAAO,CAAC,MAAM,EAAE,CACrD,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACf,CAAA;IACD,OAAO,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;AAC9B,CAAC"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The default ceiling on one tool result, in characters.
3
+ *
4
+ * A tool result is spent from the model's context window, and this surface has endpoints that can
5
+ * legitimately answer with a megabyte (a run's LLM-call bodies, an agent-context snapshot). The
6
+ * endpoints that CAN be large all take `limit` / `cursor` / `offset`, so the cap is not a loss of
7
+ * capability — but it has to be stated rather than applied silently, because a quietly shortened
8
+ * answer is one a model reports on as though it were the whole thing.
9
+ */
10
+ export declare const DEFAULT_MAX_RESULT_CHARS = 100000;
11
+ /**
12
+ * The MCP content shape, declared here so this module needs no protocol types (which keeps it
13
+ * unit-testable against plain objects).
14
+ *
15
+ * A type ALIAS, not an interface, and that is load-bearing rather than style: only an alias of an
16
+ * object literal gets TypeScript's implicit index signature, and the protocol's result type is an
17
+ * open `{ [x: string]: unknown }` record. An interface fails to assign to it, and the fix would
18
+ * otherwise be a cast at the one place a wrong shape should be caught. The generated SDK's query
19
+ * types are an alias for exactly this reason.
20
+ */
21
+ export type ToolResult = {
22
+ content: {
23
+ type: 'text';
24
+ text: string;
25
+ }[];
26
+ isError?: boolean;
27
+ };
28
+ /**
29
+ * Render an SDK result as tool content.
30
+ *
31
+ * A `204` endpoint resolves to `undefined`, which is a real answer ("it worked, there is nothing
32
+ * to return") and not an empty one, so it is SAID rather than rendered as `undefined` or as an
33
+ * empty string a model would read as a failure.
34
+ */
35
+ export declare function renderResult(value: unknown, options: {
36
+ maxChars?: number;
37
+ toolName: string;
38
+ }): ToolResult;
39
+ /**
40
+ * Render a failure as tool content.
41
+ *
42
+ * `isError` rather than a thrown JSON-RPC error, deliberately: a protocol error says the SERVER
43
+ * misbehaved and is not shown to the model, where a failed tool call is information the model is
44
+ * meant to act on. A 422 naming the field it got wrong is the most useful thing this facade ever
45
+ * returns, and throwing would hide it.
46
+ *
47
+ * The deployment's own vocabulary is passed through verbatim — `code`, `details.reason` and the
48
+ * per-field `issues` — because that is what the API guide documents and what a caller (human or
49
+ * model) can look up. Nothing here re-words a refusal into a friendlier one that means less.
50
+ */
51
+ export declare function renderError(error: unknown, options: {
52
+ toolName: string;
53
+ }): ToolResult;
54
+ //# sourceMappingURL=result.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"result.d.ts","sourceRoot":"","sources":["../src/result.ts"],"names":[],"mappings":"AAMA;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,SAAU,CAAA;AAE/C;;;;;;;;;GASG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;IACzC,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB,CAAA;AAQD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,OAAO,EACd,OAAO,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAC/C,UAAU,CAiBZ;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,UAAU,CA+BrF"}
package/dist/result.js ADDED
@@ -0,0 +1,87 @@
1
+ import { CatFactoryApiError, CatFactoryError } from '@cat-factory/sdk';
2
+ // Turning one SDK call into one MCP tool result. Two rules, and both are about a MODEL being the
3
+ // reader: what it gets back has to be honest about size, and a refusal has to say enough for the
4
+ // model to fix its own call rather than retry the same one.
5
+ /**
6
+ * The default ceiling on one tool result, in characters.
7
+ *
8
+ * A tool result is spent from the model's context window, and this surface has endpoints that can
9
+ * legitimately answer with a megabyte (a run's LLM-call bodies, an agent-context snapshot). The
10
+ * endpoints that CAN be large all take `limit` / `cursor` / `offset`, so the cap is not a loss of
11
+ * capability — but it has to be stated rather than applied silently, because a quietly shortened
12
+ * answer is one a model reports on as though it were the whole thing.
13
+ */
14
+ export const DEFAULT_MAX_RESULT_CHARS = 100_000;
15
+ function text(value, isError = false) {
16
+ return isError
17
+ ? { content: [{ type: 'text', text: value }], isError: true }
18
+ : { content: [{ type: 'text', text: value }] };
19
+ }
20
+ /**
21
+ * Render an SDK result as tool content.
22
+ *
23
+ * A `204` endpoint resolves to `undefined`, which is a real answer ("it worked, there is nothing
24
+ * to return") and not an empty one, so it is SAID rather than rendered as `undefined` or as an
25
+ * empty string a model would read as a failure.
26
+ */
27
+ export function renderResult(value, options) {
28
+ if (value === undefined) {
29
+ return text('The request succeeded. This endpoint returns no content.');
30
+ }
31
+ const maxChars = options.maxChars ?? DEFAULT_MAX_RESULT_CHARS;
32
+ const json = JSON.stringify(value, null, 2);
33
+ if (json.length <= maxChars)
34
+ return text(json);
35
+ // The note goes FIRST and names the truncation in the same breath as the remedy: what follows
36
+ // is no longer parseable JSON, and a model that starts reading at the top must know that before
37
+ // it starts, not after it has already summarised half a document as complete.
38
+ const dropped = json.length - maxChars;
39
+ const note = `[TRUNCATED] \`${options.toolName}\` returned ${json.length} characters, over this server's ` +
40
+ `${maxChars}-character limit; the last ${dropped} were dropped. What follows is the beginning ` +
41
+ 'of the response and is NOT valid JSON. Narrow the request instead of reading on: list ' +
42
+ 'endpoints take `limit` and `cursor`, and the debug text reads take `offset`.';
43
+ return text(`${note}\n\n${json.slice(0, maxChars)}`);
44
+ }
45
+ /**
46
+ * Render a failure as tool content.
47
+ *
48
+ * `isError` rather than a thrown JSON-RPC error, deliberately: a protocol error says the SERVER
49
+ * misbehaved and is not shown to the model, where a failed tool call is information the model is
50
+ * meant to act on. A 422 naming the field it got wrong is the most useful thing this facade ever
51
+ * returns, and throwing would hide it.
52
+ *
53
+ * The deployment's own vocabulary is passed through verbatim — `code`, `details.reason` and the
54
+ * per-field `issues` — because that is what the API guide documents and what a caller (human or
55
+ * model) can look up. Nothing here re-words a refusal into a friendlier one that means less.
56
+ */
57
+ export function renderError(error, options) {
58
+ if (error instanceof CatFactoryApiError) {
59
+ const lines = [
60
+ `${options.toolName} failed: HTTP ${error.status} (${error.code})`,
61
+ error.message,
62
+ ];
63
+ if (error.issues.length > 0) {
64
+ lines.push('Fields the deployment rejected:', ...error.issues.map((issue) => ` - ${issue.path ?? '(body)'}: ${issue.message}`));
65
+ }
66
+ if (error.details !== undefined && error.details !== null) {
67
+ lines.push(`details: ${JSON.stringify(error.details)}`);
68
+ }
69
+ // The request id is what a human correlates with the deployment's logs; it is worth more in
70
+ // this string than everything else here when the failure turns out to be server-side.
71
+ if (error.requestId)
72
+ lines.push(`requestId: ${error.requestId}`);
73
+ return text(lines.join('\n'), true);
74
+ }
75
+ if (error instanceof CatFactoryError) {
76
+ // A transport-level failure (connection, timeout, an undecodable body). The SDK has already
77
+ // spent its retry budget by the time this is reached, so "try again" is the caller's decision
78
+ // and not a thing to imply here.
79
+ return text(`${options.toolName} failed: ${error.name}: ${error.message}`, true);
80
+ }
81
+ // Anything else is this facade's own fault — most often an argument the tool's schema declares
82
+ // required and the host did not send. Named as such so the model retries with the argument
83
+ // rather than concluding the deployment is broken.
84
+ const message = error instanceof Error ? error.message : String(error);
85
+ return text(`${options.toolName} could not be called: ${message}`, true);
86
+ }
87
+ //# sourceMappingURL=result.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"result.js","sourceRoot":"","sources":["../src/result.ts"],"names":[],"mappings":"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"}