@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.
- package/README.md +94 -0
- package/dist/index.js +186 -0
- 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
|
+
}
|