@kubova/mcp 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.
Files changed (3) hide show
  1. package/README.md +94 -0
  2. package/dist/index.js +186 -0
  3. package/package.json +30 -0
package/README.md ADDED
@@ -0,0 +1,94 @@
1
+ # @kubova/mcp
2
+
3
+ Model Context Protocol server for the [Kubova](https://kubova-web-next.vercel.app) container loading calculator.
4
+
5
+ Lets Claude, Cursor, Cline, ChatGPT, n8n, and any other MCP-aware AI agent pack shipping containers as a tool call.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ # Get an API key first at
11
+ # https://kubova-web-next.vercel.app/dashboard/api-keys
12
+ # (Pro plan; 14-day trial available.)
13
+ ```
14
+
15
+ ## Configure
16
+
17
+ ### Claude Code / Claude Desktop
18
+
19
+ Add to `~/.claude/settings.json` (or the Claude Desktop config file):
20
+
21
+ ```json
22
+ {
23
+ "mcpServers": {
24
+ "kubova": {
25
+ "command": "npx",
26
+ "args": ["-y", "@kubova/mcp@latest"],
27
+ "env": {
28
+ "KUBOVA_API_KEY": "kbv_..."
29
+ }
30
+ }
31
+ }
32
+ }
33
+ ```
34
+
35
+ ### Cursor / Cline / Codex
36
+
37
+ Same JSON shape — point your MCP-aware client at the package via `npx`.
38
+
39
+ ## Tools exposed
40
+
41
+ | Tool | Purpose |
42
+ |---|---|
43
+ | `pack_containers` | Pack a list of cargo SKUs into one or more shipping containers. Returns placements, utilization, weight, and any unplaced pieces. |
44
+ | `verify_key` | Sanity-check the configured API key. Returns identity, scopes, and rate limit. |
45
+
46
+ ## Input schema (pack_containers)
47
+
48
+ ```ts
49
+ {
50
+ cargos: [
51
+ {
52
+ id: string,
53
+ name: string,
54
+ shape?: "box" | "cylinder",
55
+ lengthCm: number,
56
+ widthCm: number,
57
+ heightCm: number,
58
+ quantity: number,
59
+ weightKg: number,
60
+ color?: string,
61
+ includeInLoading?: boolean,
62
+ allowStacking?: boolean,
63
+ allowRotation?: boolean
64
+ }
65
+ ],
66
+ container?: {
67
+ id: string,
68
+ name: string,
69
+ innerLengthCm: number,
70
+ innerWidthCm: number,
71
+ innerHeightCm: number,
72
+ doorWidthCm: number,
73
+ doorHeightCm: number,
74
+ maxPayloadKg: number
75
+ },
76
+ containers?: [ /* multiple types — packer picks best mix */ ],
77
+ options?: {
78
+ loadingDirection?: "floor-to-top" | "right-to-left",
79
+ maxContainers?: number,
80
+ vnsTimeLimitMs?: number
81
+ }
82
+ }
83
+ ```
84
+
85
+ ## Env
86
+
87
+ | Variable | Default | Notes |
88
+ |---|---|---|
89
+ | `KUBOVA_API_KEY` | — | **Required.** Generated from the dashboard. |
90
+ | `KUBOVA_API_URL` | `https://kubova-web-next.vercel.app` | Override if self-hosting Kubova. |
91
+
92
+ ## License
93
+
94
+ MIT.
package/dist/index.js ADDED
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Kubova MCP server.
4
+ *
5
+ * Exposes the Kubova container loading calculator as an MCP server so
6
+ * Claude, Cursor, Cline, ChatGPT, n8n, and any other MCP-aware AI agent
7
+ * can pack containers as a tool call.
8
+ *
9
+ * Usage in Claude Code / Cursor settings:
10
+ *
11
+ * {
12
+ * "mcpServers": {
13
+ * "kubova": {
14
+ * "command": "npx",
15
+ * "args": ["-y", "@kubova/mcp@latest"],
16
+ * "env": { "KUBOVA_API_KEY": "kbv_..." }
17
+ * }
18
+ * }
19
+ * }
20
+ *
21
+ * Env:
22
+ * KUBOVA_API_KEY (required) — get one at https://kubova-web-next.vercel.app/dashboard/api-keys
23
+ * KUBOVA_API_URL (optional) — defaults to https://kubova-web-next.vercel.app
24
+ */
25
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
26
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
27
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
28
+ import { z } from 'zod';
29
+ const API_URL = process.env.KUBOVA_API_URL ?? 'https://kubova-web-next.vercel.app';
30
+ const API_KEY = process.env.KUBOVA_API_KEY;
31
+ if (!API_KEY) {
32
+ // eslint-disable-next-line no-console
33
+ console.error('[kubova-mcp] KUBOVA_API_KEY is required. Generate one at /dashboard/api-keys.');
34
+ process.exit(1);
35
+ }
36
+ const CargoSchema = z.object({
37
+ id: z.string(),
38
+ name: z.string(),
39
+ shape: z.enum(['box', 'cylinder']).default('box'),
40
+ lengthCm: z.number().positive(),
41
+ widthCm: z.number().positive(),
42
+ heightCm: z.number().positive(),
43
+ diameterCm: z.number().positive().optional(),
44
+ quantity: z.number().int().positive(),
45
+ weightKg: z.number().nonnegative(),
46
+ color: z.string().default('#3b82f6'),
47
+ includeInLoading: z.boolean().default(true),
48
+ allowStacking: z.boolean().default(true),
49
+ allowRotation: z.boolean().default(true),
50
+ });
51
+ const ContainerSchema = z.object({
52
+ id: z.string(),
53
+ name: z.string(),
54
+ innerLengthCm: z.number().positive(),
55
+ innerWidthCm: z.number().positive(),
56
+ innerHeightCm: z.number().positive(),
57
+ doorWidthCm: z.number().positive(),
58
+ doorHeightCm: z.number().positive(),
59
+ maxPayloadKg: z.number().positive(),
60
+ });
61
+ const PackInputSchema = z.object({
62
+ cargos: z.array(CargoSchema).min(1),
63
+ container: ContainerSchema.optional(),
64
+ containers: z.array(ContainerSchema).optional(),
65
+ options: z
66
+ .object({
67
+ loadingDirection: z.enum(['floor-to-top', 'right-to-left']).optional(),
68
+ maxContainers: z.number().int().positive().optional(),
69
+ vnsTimeLimitMs: z.number().int().nonnegative().optional(),
70
+ })
71
+ .optional(),
72
+ });
73
+ const PACK_INPUT_JSONSCHEMA = {
74
+ type: 'object',
75
+ required: ['cargos'],
76
+ properties: {
77
+ cargos: {
78
+ type: 'array',
79
+ minItems: 1,
80
+ items: {
81
+ type: 'object',
82
+ required: ['id', 'name', 'lengthCm', 'widthCm', 'heightCm', 'quantity', 'weightKg'],
83
+ properties: {
84
+ id: { type: 'string' },
85
+ name: { type: 'string' },
86
+ shape: { type: 'string', enum: ['box', 'cylinder'], default: 'box' },
87
+ lengthCm: { type: 'number' },
88
+ widthCm: { type: 'number' },
89
+ heightCm: { type: 'number' },
90
+ diameterCm: { type: 'number' },
91
+ quantity: { type: 'integer' },
92
+ weightKg: { type: 'number' },
93
+ color: { type: 'string' },
94
+ includeInLoading: { type: 'boolean', default: true },
95
+ allowStacking: { type: 'boolean', default: true },
96
+ allowRotation: { type: 'boolean', default: true },
97
+ },
98
+ },
99
+ },
100
+ container: {
101
+ type: 'object',
102
+ description: 'Single container to pack into. Use this OR containers[].',
103
+ },
104
+ containers: {
105
+ type: 'array',
106
+ description: 'Multiple container types — packer picks the best mix.',
107
+ },
108
+ options: {
109
+ type: 'object',
110
+ properties: {
111
+ loadingDirection: { type: 'string', enum: ['floor-to-top', 'right-to-left'] },
112
+ maxContainers: { type: 'integer' },
113
+ vnsTimeLimitMs: { type: 'integer' },
114
+ },
115
+ },
116
+ },
117
+ };
118
+ const server = new Server({ name: 'kubova-mcp', version: '0.1.0' }, { capabilities: { tools: {} } });
119
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
120
+ tools: [
121
+ {
122
+ name: 'pack_containers',
123
+ description: 'Pack a list of cargo SKUs into one or more shipping containers using ' +
124
+ 'the Kubova algorithm (Bischoff–Ratcliff block-loading + Parreño 2010 VNS). ' +
125
+ 'Returns per-container placements with x,y,z coordinates in cm, volume ' +
126
+ 'utilization, weight, and any unplaced pieces.',
127
+ inputSchema: PACK_INPUT_JSONSCHEMA,
128
+ },
129
+ {
130
+ name: 'generate_report',
131
+ description: 'Generate a CAD-style PDF + 3D PNG report for a packing job. ' +
132
+ 'Same input shape as pack_containers. Returns base64-encoded PDF + PNG per container ' +
133
+ 'plus the full pack result (placements, utilization, unplaced).',
134
+ inputSchema: PACK_INPUT_JSONSCHEMA,
135
+ },
136
+ {
137
+ name: 'verify_key',
138
+ description: 'Verify the configured API key. Returns the key identity, scopes, and rate limit.',
139
+ inputSchema: { type: 'object', properties: {} },
140
+ },
141
+ ],
142
+ }));
143
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
144
+ const { name, arguments: args } = req.params;
145
+ if (name === 'verify_key') {
146
+ const res = await fetch(`${API_URL}/api/v1/me`, {
147
+ headers: { Authorization: `Bearer ${API_KEY}` },
148
+ });
149
+ const text = await res.text();
150
+ return {
151
+ content: [{ type: 'text', text }],
152
+ isError: !res.ok,
153
+ };
154
+ }
155
+ if (name === 'pack_containers' || name === 'generate_report') {
156
+ const parsed = PackInputSchema.safeParse(args);
157
+ if (!parsed.success) {
158
+ return {
159
+ content: [{ type: 'text', text: `Invalid input: ${parsed.error.message}` }],
160
+ isError: true,
161
+ };
162
+ }
163
+ const endpoint = name === 'pack_containers' ? '/api/v1/pack' : '/api/v1/report';
164
+ const res = await fetch(`${API_URL}${endpoint}`, {
165
+ method: 'POST',
166
+ headers: {
167
+ Authorization: `Bearer ${API_KEY}`,
168
+ 'Content-Type': 'application/json',
169
+ },
170
+ body: JSON.stringify(parsed.data),
171
+ });
172
+ const text = await res.text();
173
+ return {
174
+ content: [{ type: 'text', text }],
175
+ isError: !res.ok,
176
+ };
177
+ }
178
+ return {
179
+ content: [{ type: 'text', text: `Unknown tool: ${name}` }],
180
+ isError: true,
181
+ };
182
+ });
183
+ const transport = new StdioServerTransport();
184
+ await server.connect(transport);
185
+ // eslint-disable-next-line no-console
186
+ console.error('[kubova-mcp] connected on stdio');
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@kubova/mcp",
3
+ "version": "0.1.0",
4
+ "description": "Model Context Protocol server for the Kubova container loading calculator",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "kubova-mcp": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "dev": "tsx src/index.ts",
16
+ "prepublishOnly": "pnpm build"
17
+ },
18
+ "dependencies": {
19
+ "@modelcontextprotocol/sdk": "^1.0.0",
20
+ "zod": "^3.25.0"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^22.0.0",
24
+ "tsx": "^4.19.0",
25
+ "typescript": "^5.9.0"
26
+ },
27
+ "engines": {
28
+ "node": ">=20"
29
+ }
30
+ }