@cutoptim/mcp 0.0.0-stage → 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 CutOptim
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 CHANGED
@@ -1,3 +1,73 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ <p align="center">
2
+ <a href="https://cutoptim.com/engine"><img src="https://raw.githubusercontent.com/cutoptim/engine-mcp/main/.github/logo.png" width="88" height="88" alt="CutOptim"></a>
3
+ </p>
4
+
5
+ <h1 align="center">CutOptim MCP server</h1>
6
+
7
+ <p align="center">
8
+ Cutting optimization as tools for AI assistants: <b>2D panels</b>, <b>1D bars</b>, <b>timber</b> and <b>true-shape nesting</b>,<br>
9
+ through the <a href="https://cutoptim.com/engine">CutOptim Engine API</a> and the Model Context Protocol.
10
+ </p>
11
+
12
+ <p align="center">
13
+ <a href="https://cutoptim.com/engine/reference/guides/mcp">MCP guide</a> ·
14
+ <a href="https://cutoptim.com/engine/reference">API reference</a> ·
15
+ <a href="https://cutoptim.com/dashboard?api=test#api-keys">Get a free test key</a>
16
+ </p>
17
+
18
+ ---
19
+
20
+ ## Two ways to connect
21
+
22
+ **Hosted (no install)** — point your MCP client at `https://api.cutoptim.com/mcp` with the header
23
+ `Authorization: Bearer <your API key>`. Configurations for Claude Code, Cursor, VS Code and Claude Desktop are in the
24
+ [MCP guide](https://cutoptim.com/engine/reference/guides/mcp).
25
+
26
+ **Local (this package, stdio)** — runs on your machine and calls the same API with your key:
27
+
28
+ ```json
29
+ {
30
+ "mcpServers": {
31
+ "cutoptim": {
32
+ "command": "npx",
33
+ "args": ["-y", "@cutoptim/mcp"],
34
+ "env": { "CUTOPTIM_API_KEY": "co_test_YOUR_KEY" }
35
+ }
36
+ }
37
+ }
38
+ ```
39
+
40
+ Claude Code: `claude mcp add cutoptim --env CUTOPTIM_API_KEY=co_test_YOUR_KEY -- npx -y @cutoptim/mcp`
41
+
42
+ | Variable | Meaning |
43
+ |---|---|
44
+ | `CUTOPTIM_API_KEY` | your key: `co_live_…`, `co_test_…` (never billed) or the public demo key. Without one, only the `validate_*` tools work. |
45
+ | `CUTOPTIM_BASE_URL` | default `https://api.cutoptim.com` |
46
+
47
+ Node 18 or newer.
48
+
49
+ ## Tools
50
+
51
+ | Tool | What it does | Cost |
52
+ |---|---|---|
53
+ | `optimize_2d` | rectangular parts from sheets: layout, cut plan, yield | one API call |
54
+ | `optimize_1d` | lengths from bars, tubes, profiles | one API call |
55
+ | `optimize_wood` | timber parts by cross-section | one API call |
56
+ | `optimize_nest` | free-form polygon parts on sheets | one API call |
57
+ | `validate_2d` · `validate_1d` · `validate_wood` · `validate_nest` | check a request without solving it | free |
58
+ | `get_job` | read an async `engine: "max"` job | free |
59
+ | `get_usage` | calls used and left this month | free |
60
+
61
+ The arguments of each tool are exactly the request body of its endpoint — the schemas are generated from the
62
+ [OpenAPI document](https://cutoptim.com/engine/openapi.json). Every result carries a short summary for the assistant
63
+ and the API's JSON answer as `structuredContent`.
64
+
65
+ ## Quota and billing
66
+
67
+ One tool call is one Engine API call: the same key, quota, rate limits and billing as calling the REST API yourself.
68
+ The client sends an `Idempotency-Key` with every optimize call, so its own retries are never charged twice.
69
+
70
+ ## License and support
71
+
72
+ MIT. This repository is published from the CutOptim monorepo; report a bug in
73
+ [Issues](https://github.com/cutoptim/engine-mcp/issues) or write to support@cutoptim.com.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ // npx @cutoptim/mcp — the CutOptim MCP server over stdio.
3
+ // CUTOPTIM_API_KEY your key (co_live_…, co_test_…, or the public demo key); not needed for validate_*
4
+ // CUTOPTIM_BASE_URL default https://api.cutoptim.com
5
+ // stdout carries the MCP protocol only; diagnostics go to stderr.
6
+ import { CutOptim } from '@cutoptim/engine';
7
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
8
+ import { createCutOptimToolServer, VERSION } from './server.js';
9
+ const client = new CutOptim({
10
+ apiKey: process.env.CUTOPTIM_API_KEY || undefined,
11
+ baseUrl: process.env.CUTOPTIM_BASE_URL || undefined,
12
+ });
13
+ const server = createCutOptimToolServer(client);
14
+ await server.connect(new StdioServerTransport());
15
+ console.error(`cutoptim-mcp ${VERSION} ready (stdio)${process.env.CUTOPTIM_API_KEY ? '' : ' — no CUTOPTIM_API_KEY: only validate_* will work'}`);
@@ -0,0 +1,53 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { type CallToolResult } from '@modelcontextprotocol/sdk/types.js';
3
+ export interface ToolDef {
4
+ name: string;
5
+ title: string;
6
+ description: string;
7
+ inputSchema: Record<string, unknown>;
8
+ outputSchema?: Record<string, unknown>;
9
+ annotations: {
10
+ title: string;
11
+ readOnlyHint: boolean;
12
+ destructiveHint: boolean;
13
+ idempotentHint: boolean;
14
+ openWorldHint: boolean;
15
+ };
16
+ /** The REST operation behind the tool. `pathParam` names the argument that fills the `{id}` segment. */
17
+ http: {
18
+ method: 'GET' | 'POST';
19
+ path: string;
20
+ operationId: string;
21
+ pathParam?: string;
22
+ };
23
+ }
24
+ /** The REST request a tool call becomes. `args` are the tool arguments as received (a client SDK may dispatch on them). */
25
+ export interface ApiRequest {
26
+ method: 'GET' | 'POST';
27
+ path: string;
28
+ body?: unknown;
29
+ operationId: string;
30
+ args: Record<string, unknown>;
31
+ }
32
+ export interface ApiAnswer {
33
+ status: number;
34
+ body: unknown;
35
+ /** X-RateLimit-Remaining when the API sent it (monthly calls left, or the demo key's daily allowance). */
36
+ remaining?: number | null;
37
+ /** True when the public demo key answered. */
38
+ demo?: boolean;
39
+ }
40
+ export type ApiCall = (req: ApiRequest, signal?: AbortSignal) => Promise<ApiAnswer>;
41
+ export declare const SERVER_NAME = "cutoptim";
42
+ export declare const SERVER_INSTRUCTIONS: string;
43
+ export declare function createCutOptimMcpServer(opts: {
44
+ tools: ToolDef[];
45
+ call: ApiCall;
46
+ version: string;
47
+ }): Server;
48
+ /** Arguments → the documented REST request. POST tools send the arguments verbatim as the JSON body. */
49
+ export declare function toApiRequest(tool: ToolDef, args: Record<string, unknown>): ApiRequest | {
50
+ error: string;
51
+ };
52
+ export declare function toToolResult(tool: ToolDef, a: ApiAnswer): CallToolResult;
53
+ export declare function summarize(tool: ToolDef, a: ApiAnswer, b: Record<string, any>): string;
@@ -0,0 +1,167 @@
1
+ // GENERATED copy of the hosted server's MCP core by scripts/gen-mcp.ts — DO NOT EDIT.
2
+ // CutOptim MCP core: the tool dispatcher and the result formatter.
3
+ //
4
+ // ONE file, shared by the hosted server (https://api.cutoptim.com/mcp) and the local stdio package
5
+ // (@cutoptim/mcp). The package carries a byte-identical copy (core.gen.ts, written by `npm run gen:mcp`),
6
+ // so this file imports nothing but the MCP SDK and must stay free of server-only code.
7
+ //
8
+ // A tool call is exactly one Engine API call: the dispatcher turns the arguments into the HTTP request the
9
+ // REST API documents and hands it to `call`, which the host implements (the hosted server: a loopback request
10
+ // into the engine itself, so auth, rate limits, quota and billing are the REST ones; the stdio package: the
11
+ // official TypeScript client). Nothing here decides what is billed.
12
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
13
+ import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from '@modelcontextprotocol/sdk/types.js';
14
+ export const SERVER_NAME = 'cutoptim';
15
+ export const SERVER_INSTRUCTIONS = 'CutOptim cutting optimization: 2D panels (optimize_2d), 1D bars and profiles (optimize_1d), timber with ' +
16
+ 'cross-sections (optimize_wood) and true-shape nesting (optimize_nest). Every optimize_* call is one metered ' +
17
+ 'Engine API call; validate_* is free and checks a request without solving it, so validate first when unsure. ' +
18
+ 'All sizes are unit-agnostic numbers: keep one unit (e.g. mm) across parts, stock and kerf. ' +
19
+ 'engine "max" runs asynchronously: the answer is a jobId, poll it with get_job. ' +
20
+ 'get_usage shows the calls left this month. Reference: https://cutoptim.com/engine/reference';
21
+ /** Export strings that are elided from the JSON text block (they stay in structuredContent). */
22
+ const EXPORT_FIELDS = ['svg', 'dxf', 'csv', 'pdf', 'labelsPdf'];
23
+ const MAX_TABLE_ROWS = 25;
24
+ export function createCutOptimMcpServer(opts) {
25
+ const byName = new Map(opts.tools.map((t) => [t.name, t]));
26
+ const server = new Server({ name: SERVER_NAME, title: 'CutOptim Engine', version: opts.version }, { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS });
27
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
28
+ tools: opts.tools.map((t) => ({
29
+ name: t.name, title: t.title, description: t.description,
30
+ inputSchema: t.inputSchema,
31
+ ...(t.outputSchema ? { outputSchema: t.outputSchema } : {}),
32
+ annotations: t.annotations,
33
+ })),
34
+ }));
35
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
36
+ const tool = byName.get(request.params.name);
37
+ if (!tool)
38
+ throw new McpError(ErrorCode.InvalidParams, `unknown tool: ${request.params.name}`);
39
+ const args = (request.params.arguments ?? {});
40
+ const req = toApiRequest(tool, args);
41
+ if ('error' in req)
42
+ return errorResult(req.error);
43
+ let answer;
44
+ try {
45
+ answer = await opts.call(req, extra.signal);
46
+ }
47
+ catch (e) {
48
+ return errorResult(`The CutOptim Engine API could not be reached: ${e instanceof Error ? e.message : String(e)}`);
49
+ }
50
+ return toToolResult(tool, answer);
51
+ });
52
+ return server;
53
+ }
54
+ /** Arguments → the documented REST request. POST tools send the arguments verbatim as the JSON body. */
55
+ export function toApiRequest(tool, args) {
56
+ const { method, path, operationId, pathParam } = tool.http;
57
+ if (pathParam) {
58
+ const v = args[pathParam];
59
+ if (typeof v !== 'string' || v === '')
60
+ return { error: `${tool.name} needs "${pathParam}" (a string).` };
61
+ return { method, operationId, args, path: path.replace(/\{[^}]+\}/, encodeURIComponent(v)) };
62
+ }
63
+ return method === 'POST' ? { method, path, operationId, args, body: args } : { method, path, operationId, args };
64
+ }
65
+ function errorResult(text) {
66
+ return { isError: true, content: [{ type: 'text', text }] };
67
+ }
68
+ export function toToolResult(tool, a) {
69
+ const body = (a.body && typeof a.body === 'object' ? a.body : {});
70
+ if (a.status < 200 || a.status > 299) {
71
+ const code = typeof body.error === 'string' ? body.error : `http_${a.status}`;
72
+ const msg = typeof body.message === 'string' ? body.message : 'no message';
73
+ const lines = [`CutOptim API error ${a.status} ${code}: ${msg}`];
74
+ if (body.details != null)
75
+ lines.push(`details: ${JSON.stringify(body.details)}`);
76
+ if (a.status === 401)
77
+ lines.push('Send your API key as "Authorization: Bearer co_live_… / co_test_…" in the MCP client configuration. A free test key (never billed): https://cutoptim.com/dashboard?api=test#api-keys');
78
+ return errorResult(lines.join('\n'));
79
+ }
80
+ const content = [{ type: 'text', text: summarize(tool, a, body) }];
81
+ const elided = { ...body };
82
+ for (const f of EXPORT_FIELDS) {
83
+ if (typeof body[f] === 'string')
84
+ elided[f] = `[${f}: ${body[f].length} characters, in structuredContent]`;
85
+ }
86
+ content.push({ type: 'text', text: JSON.stringify(elided) });
87
+ if (typeof body.svg === 'string') {
88
+ content.push({ type: 'resource', resource: { uri: `cutoptim://layout/${tool.name}.svg`, mimeType: 'image/svg+xml', text: body.svg } });
89
+ }
90
+ return { content, structuredContent: body };
91
+ }
92
+ // ── human summaries ────────────────────────────────────────────────────────────────────────────────
93
+ const n = (v) => (typeof v === 'number' && Number.isFinite(v) ? String(Math.round(v * 100) / 100) : '–');
94
+ const list = (v) => (Array.isArray(v) ? v : []);
95
+ export function summarize(tool, a, b) {
96
+ const out = [];
97
+ const op = tool.http.operationId;
98
+ if (a.status === 202 && typeof b.jobId === 'string') {
99
+ out.push(`Job queued: ${b.jobId} (engine ${b.engine ?? 'max'}, mode ${b.mode ?? '?'}). Poll it with get_job after about ${Math.ceil((Number(b.pollAfterMs) || 1000) / 1000)} s.`);
100
+ }
101
+ else if (op.startsWith('validate')) {
102
+ out.push(b.valid === true
103
+ ? `Valid ${b.mode ?? ''} request: ${n(b.parts?.rows)} part rows (${n(b.parts?.total)} pieces), ${n(b.stock?.rows)} stock rows. Not solved and not billed.`
104
+ : `Request is not valid: ${JSON.stringify(b)}`);
105
+ warnings(out, b);
106
+ }
107
+ else if (op === 'getUsage') {
108
+ out.push(`Plan ${b.plan ?? '?'}: ${n(b.used)} of ${n(b.limit)} calls used this period, ${n(b.remaining)} left (period ends ${b.periodEnd ?? '?'}, key ${b.keyPrefix ?? '?'}…).`);
109
+ }
110
+ else if (op === 'getJob') {
111
+ out.push(`Job ${b.jobId ?? '?'}: ${b.status ?? '?'} (engine ${b.engine ?? '?'}, mode ${b.mode ?? '?'}).`);
112
+ if (b.status === 'queued' || b.status === 'running')
113
+ out.push('Not finished yet: poll get_job again in a few seconds.');
114
+ if (b.status === 'failed' && b.error)
115
+ out.push(`Error: ${b.error}`);
116
+ if (b.result && typeof b.result === 'object')
117
+ planSummary(out, b.mode === 'nest' ? 'nest' : '2d', b.result);
118
+ }
119
+ else {
120
+ planSummary(out, op === 'optimize1d' ? '1d' : op === 'optimizeWood' ? 'wood' : op === 'optimizeNest' ? 'nest' : '2d', b);
121
+ }
122
+ if (a.remaining != null)
123
+ out.push(a.demo ? `Demo key: ${a.remaining} demo calls left today from this address.` : `Calls left this month: ${a.remaining}.`);
124
+ return out.join('\n');
125
+ }
126
+ function planSummary(out, mode, b) {
127
+ const m = b.metrics ?? {};
128
+ const price = typeof m.totalPrice === 'number' && m.totalPrice > 0 ? `, total price ${n(m.totalPrice)}` : '';
129
+ if (mode === '2d') {
130
+ out.push(`2D plan (engine ${b.engine ?? '?'}): ${n(m.placed)}/${n(m.total)} parts on ${n(m.sheetCount)} sheets, yield ${n(m.yieldPct)}%, ${n(m.cutLines)} cuts${price}.`);
131
+ table(out, ['sheet', 'size', 'parts', 'offcuts'], list(b.sheets).map((s, i) => [String(i + 1), `${n(s.w)} × ${n(s.h)}`, String(list(s.parts).length), String(list(s.offcuts).length)]));
132
+ }
133
+ else if (mode === '1d') {
134
+ out.push(`1D plan (engine ${b.engine ?? '?'}): ${n(m.placed)}/${n(m.total)} parts on ${n(m.rodCount)} bars, yield ${n(m.yieldPct)}%, ${n(m.cuts)} cuts${price}.`);
135
+ table(out, ['bar', 'length', 'parts', 'remaining'], list(b.rods).map((r, i) => [String(i + 1), n(r.length), String(list(r.parts).length), n(r.remaining)]));
136
+ }
137
+ else if (mode === 'wood') {
138
+ out.push(`Timber plan (engine ${b.engine ?? '?'}): ${n(m.placed)}/${n(m.total)} parts in ${n(m.sectionCount)} cross-sections on ${n(m.rodCount)} pieces of stock, yield ${n(m.yieldPct)}%${price}.`);
139
+ table(out, ['section', 'stock', 'pieces', 'yield %'], list(b.sections).map((s) => [String(s.section ?? `${n(s.sw)}×${n(s.sh)}`), String(s.stockName ?? '–'), String(list(s.rods).length), n(s.metrics?.yieldPct)]));
140
+ if (list(b.unmatched).length)
141
+ out.push(`No stock for: ${list(b.unmatched).map((u) => u.section ?? u.name ?? JSON.stringify(u)).join(', ')}`);
142
+ }
143
+ else {
144
+ out.push(`Nesting plan (engine ${b.engine ?? '?'}): ${n(m.placed)}/${n(m.total)} parts on ${n(m.sheetCount)} sheets, density ${n((Number(m.density) || 0) * 100)}%${price}.`);
145
+ table(out, ['sheet', 'size', 'parts', 'density %'], list(b.sheets).map((s, i) => [String(i + 1), `${n(s.w)} × ${n(s.h)}`, String(list(s.parts).length), n((Number(s.density) || 0) * 100)]));
146
+ }
147
+ const unplaced = list(b.unplaced);
148
+ if (unplaced.length)
149
+ out.push(`Not placed (${unplaced.length}): ${unplaced.slice(0, 10).map((u) => `${u.name ?? u.id ?? '?'}${u.qty ? ` ×${u.qty}` : ''}${u.reason ? ` (${u.reason})` : ''}`).join(', ')}${unplaced.length > 10 ? ', …' : ''}`);
150
+ warnings(out, b);
151
+ if (typeof b.svg === 'string')
152
+ out.push('An SVG drawing of the layout is attached.');
153
+ }
154
+ function warnings(out, b) {
155
+ const w = list(b.warnings).filter((x) => typeof x === 'string');
156
+ if (w.length)
157
+ out.push(`Warnings: ${w.join(' | ')}`);
158
+ }
159
+ function table(out, head, rows) {
160
+ if (!rows.length)
161
+ return;
162
+ out.push(`| ${head.join(' | ')} |`, `|${head.map(() => '---').join('|')}|`);
163
+ for (const r of rows.slice(0, MAX_TABLE_ROWS))
164
+ out.push(`| ${r.join(' | ')} |`);
165
+ if (rows.length > MAX_TABLE_ROWS)
166
+ out.push(`(${rows.length - MAX_TABLE_ROWS} more rows in the JSON)`);
167
+ }
@@ -0,0 +1,56 @@
1
+ import { type ApiAnswer, type ApiRequest } from './core.gen.js';
2
+ import { TOOLS } from './tools.gen.js';
3
+ import { VERSION } from './version.js';
4
+ export { TOOLS, VERSION };
5
+ type Opts = {
6
+ signal?: AbortSignal;
7
+ };
8
+ /** The part of the @cutoptim/engine client (`new CutOptim(...)`) the tools use. */
9
+ export interface EngineClient {
10
+ optimize2d(body: any, opts?: Opts): Promise<unknown>;
11
+ optimize1d(body: any, opts?: Opts): Promise<unknown>;
12
+ optimizeWood(body: any, opts?: Opts): Promise<unknown>;
13
+ optimizeNest(body: any, opts?: Opts): Promise<unknown>;
14
+ validate2d(body: any, opts?: Opts): Promise<unknown>;
15
+ validate1d(body: any, opts?: Opts): Promise<unknown>;
16
+ validateWood(body: any, opts?: Opts): Promise<unknown>;
17
+ validateNest(body: any, opts?: Opts): Promise<unknown>;
18
+ getJob(jobId: string, opts?: Opts): Promise<unknown>;
19
+ usage(opts?: Opts): Promise<unknown>;
20
+ }
21
+ export declare function dispatch(client: EngineClient, req: ApiRequest, signal?: AbortSignal): Promise<ApiAnswer>;
22
+ /** An MCP server (not yet connected) whose tools call the CutOptim Engine API through `client`. */
23
+ export declare function createCutOptimToolServer(client: EngineClient): import("@modelcontextprotocol/sdk/server").Server<{
24
+ method: string;
25
+ params?: {
26
+ [x: string]: unknown;
27
+ _meta?: {
28
+ [x: string]: unknown;
29
+ progressToken?: string | number | undefined;
30
+ "io.modelcontextprotocol/related-task"?: {
31
+ taskId: string;
32
+ } | undefined;
33
+ } | undefined;
34
+ } | undefined;
35
+ }, {
36
+ method: string;
37
+ params?: {
38
+ [x: string]: unknown;
39
+ _meta?: {
40
+ [x: string]: unknown;
41
+ progressToken?: string | number | undefined;
42
+ "io.modelcontextprotocol/related-task"?: {
43
+ taskId: string;
44
+ } | undefined;
45
+ } | undefined;
46
+ } | undefined;
47
+ }, {
48
+ [x: string]: unknown;
49
+ _meta?: {
50
+ [x: string]: unknown;
51
+ progressToken?: string | number | undefined;
52
+ "io.modelcontextprotocol/related-task"?: {
53
+ taskId: string;
54
+ } | undefined;
55
+ } | undefined;
56
+ }>;
package/dist/server.js ADDED
@@ -0,0 +1,43 @@
1
+ // @cutoptim/mcp — the CutOptim tools as a local MCP server, on top of the official @cutoptim/engine client.
2
+ //
3
+ // The tool list (tools.gen.ts) and the dispatcher/formatter (core.gen.ts) are the ones the hosted server at
4
+ // https://api.cutoptim.com/mcp runs — generated from the same source, so both answer the same way. Every tool call is
5
+ // exactly one Engine API call through the client: same key, quota and billing as calling the REST API yourself; the
6
+ // client adds an Idempotency-Key to optimize calls, so its own retries are never charged twice.
7
+ import { createCutOptimMcpServer } from './core.gen.js';
8
+ import { TOOLS } from './tools.gen.js';
9
+ import { VERSION } from './version.js';
10
+ export { TOOLS, VERSION };
11
+ const OPS = {
12
+ optimize2d: (c, a, o) => c.optimize2d(a, o),
13
+ optimize1d: (c, a, o) => c.optimize1d(a, o),
14
+ optimizeWood: (c, a, o) => c.optimizeWood(a, o),
15
+ optimizeNest: (c, a, o) => c.optimizeNest(a, o),
16
+ validate2D: (c, a, o) => c.validate2d(a, o),
17
+ validate1D: (c, a, o) => c.validate1d(a, o),
18
+ validateWood: (c, a, o) => c.validateWood(a, o),
19
+ validateNest: (c, a, o) => c.validateNest(a, o),
20
+ getJob: (c, a, o) => c.getJob(String(a.jobId), o),
21
+ getUsage: (c, _a, o) => c.usage(o),
22
+ };
23
+ const isJobAccepted = (r) => !!r && typeof r === 'object' && typeof r.jobId === 'string' && r.status === 'queued';
24
+ export async function dispatch(client, req, signal) {
25
+ const op = OPS[req.operationId];
26
+ if (!op)
27
+ return { status: 400, body: { error: 'invalid_request', message: `no client method for ${req.operationId}` } };
28
+ try {
29
+ const body = await op(client, req.args, { signal });
30
+ return { status: req.operationId.startsWith('optimize') && isJobAccepted(body) ? 202 : 200, body };
31
+ }
32
+ catch (e) {
33
+ // CutOptimError (status, code, details) → the API's error envelope; anything else (network) propagates.
34
+ if (e && typeof e.status === 'number' && typeof e.code === 'string') {
35
+ return { status: e.status || 504, body: { error: e.code, message: e.message, ...(e.details != null ? { details: e.details } : {}) } };
36
+ }
37
+ throw e;
38
+ }
39
+ }
40
+ /** An MCP server (not yet connected) whose tools call the CutOptim Engine API through `client`. */
41
+ export function createCutOptimToolServer(client) {
42
+ return createCutOptimMcpServer({ tools: TOOLS, version: VERSION, call: (req, signal) => dispatch(client, req, signal) });
43
+ }
@@ -0,0 +1,2 @@
1
+ import type { ToolDef } from './core.gen.js';
2
+ export declare const TOOLS: ToolDef[];