@harukibox/mcp 0.1.1 → 0.2.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 +12 -0
- package/bin/harukibox-mcp.js +1 -1
- package/dist/index.js +253 -237
- package/dist/protocol.js +129 -0
- package/dist/tools.js +172 -0
- package/package.json +10 -8
package/README.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
Model Context Protocol server for [harukibox](https://harukibox.com) — let your AI agent manage your 代購 inventory, orders, buyers and shipments through the official Agent API.
|
|
4
4
|
|
|
5
|
+
## 協定版本
|
|
6
|
+
|
|
7
|
+
實作 **MCP 2026-07-28**(stateless、`server/discover`、`resultType`、cacheable
|
|
8
|
+
`tools/list`),同時維持 **dual-era**:舊版的 `initialize` handshake 照樣回應。
|
|
9
|
+
|
|
10
|
+
也就是說現在的 Claude Desktop / Claude Code(仍在 2025-11-25 以前)**不用改任何設定
|
|
11
|
+
就能繼續用**;等 client 升級到 2026-07-28 後會自動走新路徑,一樣不用改設定。
|
|
12
|
+
|
|
13
|
+
> `@modelcontextprotocol/sdk` 最新的 1.30.0 仍停在 `2025-11-25`,沒有
|
|
14
|
+
> `server/discover` / `resultType` / `CacheableResult`,所以本 server 直接輸出 wire
|
|
15
|
+
> format 而不透過 SDK(也因此本套件零 runtime 相依)。SDK 支援新版後會換回去。
|
|
16
|
+
|
|
5
17
|
## 安裝
|
|
6
18
|
|
|
7
19
|
```bash
|
package/bin/harukibox-mcp.js
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
// Built entry point — assumes `npm run build` has produced dist/.
|
|
3
3
|
import('../dist/index.js').catch((err) => {
|
|
4
4
|
console.error('[harukibox-mcp] failed to start:', err?.message || err);
|
|
5
|
-
console.error('[harukibox-mcp] did you run `npm run build` inside packages/
|
|
5
|
+
console.error('[harukibox-mcp] did you run `npm run build` inside packages/mcp?');
|
|
6
6
|
process.exit(1);
|
|
7
7
|
});
|
package/dist/index.js
CHANGED
|
@@ -1,20 +1,45 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* harukibox MCP server.
|
|
3
|
+
* harukibox MCP server (stdio transport).
|
|
4
4
|
*
|
|
5
5
|
* Connect this server to Claude Desktop, Claude Code or any MCP-compatible
|
|
6
6
|
* client. It exposes the harukibox Agent API as MCP tools.
|
|
7
7
|
*
|
|
8
|
+
* Protocol: **dual-era**.
|
|
9
|
+
* - Modern (2026-07-28): stateless. No `initialize`; every request carries
|
|
10
|
+
* its protocol version and client capabilities in `_meta`. Implements the
|
|
11
|
+
* mandatory `server/discover` RPC. Every result carries `resultType`.
|
|
12
|
+
* - Legacy (2025-11-25 and earlier): the `initialize` handshake still works,
|
|
13
|
+
* so today's clients — none of which speak 2026-07-28 yet — keep working
|
|
14
|
+
* unchanged.
|
|
15
|
+
*
|
|
8
16
|
* Authentication: set HARUKIBOX_TOKEN env var (run `harukibox login` from
|
|
9
17
|
* @harukibox/cli to obtain one via OAuth). Base URL can be overridden via
|
|
10
18
|
* HARUKIBOX_BASE_URL.
|
|
11
19
|
*/
|
|
12
|
-
import {
|
|
13
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
14
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
20
|
+
import { createInterface } from 'node:readline';
|
|
15
21
|
import { HarukiAgentClient } from './api-client.js';
|
|
22
|
+
import { findTool, TOOLS, toolDescriptor } from './tools.js';
|
|
23
|
+
import { buildCacheableResult, buildResult, getClientInfo, getDeclaredProtocolVersion, INTERNAL_ERROR, INVALID_REQUEST, LATEST_PROTOCOL_VERSION, METHOD_NOT_FOUND, negotiateLegacyVersion, PARSE_ERROR, resolveEra, SUPPORTED_PROTOCOL_VERSIONS, UNSUPPORTED_PROTOCOL_VERSION, unsupportedVersionData, } from './protocol.js';
|
|
16
24
|
const SERVER_NAME = 'harukibox';
|
|
17
|
-
const SERVER_VERSION = '0.
|
|
25
|
+
const SERVER_VERSION = '0.2.0';
|
|
26
|
+
const SERVER_IDENTITY = { name: SERVER_NAME, version: SERVER_VERSION };
|
|
27
|
+
/**
|
|
28
|
+
* Natural-language guidance surfaced by `server/discover`. Clients may feed it
|
|
29
|
+
* to the model, so it covers what tool descriptions can't: call order and the
|
|
30
|
+
* multi-tenant caveat.
|
|
31
|
+
*/
|
|
32
|
+
const INSTRUCTIONS = [
|
|
33
|
+
'harukibox is a Japan-import (代購) inventory system: products, orders (registrations), buyers and shipments.',
|
|
34
|
+
'Call haruki_me first to confirm which store/organization the token is bound to before acting on data.',
|
|
35
|
+
'All tools operate only within that one organization; ids from another store return 404.',
|
|
36
|
+
'List endpoints are paginated — limit is capped at 100.',
|
|
37
|
+
].join(' ');
|
|
38
|
+
/**
|
|
39
|
+
* How long a client may cache `tools/list` / `server/discover`. The tool list
|
|
40
|
+
* is static per release, so an hour is safe and keeps prompt caches warm.
|
|
41
|
+
*/
|
|
42
|
+
const LIST_TTL_MS = 3_600_000;
|
|
18
43
|
function getEnv(name, fallback) {
|
|
19
44
|
const v = process.env[name];
|
|
20
45
|
if (v && v.length > 0)
|
|
@@ -47,247 +72,238 @@ const client = new HarukiAgentClient({
|
|
|
47
72
|
baseUrl,
|
|
48
73
|
userAgent: `${SERVER_NAME}-mcp/${SERVER_VERSION}`,
|
|
49
74
|
});
|
|
75
|
+
/* ------------------------------------------------------------------ state */
|
|
50
76
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* prefer this over tool name when displaying.
|
|
54
|
-
* - readOnlyHint: true → no observable state mutation.
|
|
55
|
-
* - destructiveHint: true → this can permanently change data the user cares about.
|
|
56
|
-
* Anthropic review requires destructive write tools to be marked.
|
|
57
|
-
* - openWorldHint: true → tool reaches out beyond the local process (HTTP API).
|
|
77
|
+
* Set once a client sends `initialize`. Per spec, that request selects legacy
|
|
78
|
+
* semantics for the lifetime of the stdio process.
|
|
58
79
|
*/
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
ipId: { type: 'string' },
|
|
127
|
-
locationId: { type: 'string' },
|
|
128
|
-
},
|
|
129
|
-
required: ['name', 'sku'],
|
|
130
|
-
additionalProperties: false,
|
|
131
|
-
},
|
|
132
|
-
annotations: {
|
|
133
|
-
title: 'Create product',
|
|
134
|
-
readOnlyHint: false,
|
|
135
|
-
destructiveHint: false, // 新建不算破壞性
|
|
136
|
-
openWorldHint: true,
|
|
137
|
-
},
|
|
138
|
-
},
|
|
139
|
-
{
|
|
140
|
-
name: 'haruki_list_registrations',
|
|
141
|
-
title: 'List registrations (orders)',
|
|
142
|
-
description: 'List orders (registrations) for the connected store. Filter by status or buyer.',
|
|
143
|
-
inputSchema: {
|
|
144
|
-
type: 'object',
|
|
145
|
-
properties: {
|
|
146
|
-
page: { type: 'integer', minimum: 1, default: 1 },
|
|
147
|
-
limit: { type: 'integer', minimum: 1, maximum: 100, default: 20 },
|
|
148
|
-
status: { type: 'string' },
|
|
149
|
-
buyerId: { type: 'string' },
|
|
150
|
-
},
|
|
151
|
-
additionalProperties: false,
|
|
152
|
-
},
|
|
153
|
-
annotations: {
|
|
154
|
-
title: 'List registrations',
|
|
155
|
-
readOnlyHint: true,
|
|
156
|
-
destructiveHint: false,
|
|
157
|
-
openWorldHint: true,
|
|
158
|
-
},
|
|
159
|
-
},
|
|
160
|
-
{
|
|
161
|
-
name: 'haruki_list_buyers',
|
|
162
|
-
title: 'List buyers',
|
|
163
|
-
description: 'List buyers in the connected store. Search matches across name / phone / email.',
|
|
164
|
-
inputSchema: {
|
|
165
|
-
type: 'object',
|
|
166
|
-
properties: {
|
|
167
|
-
page: { type: 'integer', minimum: 1, default: 1 },
|
|
168
|
-
limit: { type: 'integer', minimum: 1, maximum: 100, default: 20 },
|
|
169
|
-
search: { type: 'string' },
|
|
170
|
-
},
|
|
171
|
-
additionalProperties: false,
|
|
172
|
-
},
|
|
173
|
-
annotations: {
|
|
174
|
-
title: 'List buyers',
|
|
175
|
-
readOnlyHint: true,
|
|
176
|
-
destructiveHint: false,
|
|
177
|
-
openWorldHint: true,
|
|
178
|
-
},
|
|
179
|
-
},
|
|
180
|
-
{
|
|
181
|
-
name: 'haruki_list_shippings',
|
|
182
|
-
title: 'List shipments',
|
|
183
|
-
description: 'List shipments. Filter by status if provided.',
|
|
184
|
-
inputSchema: {
|
|
185
|
-
type: 'object',
|
|
186
|
-
properties: {
|
|
187
|
-
page: { type: 'integer', minimum: 1, default: 1 },
|
|
188
|
-
limit: { type: 'integer', minimum: 1, maximum: 100, default: 20 },
|
|
189
|
-
status: { type: 'string' },
|
|
190
|
-
},
|
|
191
|
-
additionalProperties: false,
|
|
192
|
-
},
|
|
193
|
-
annotations: {
|
|
194
|
-
title: 'List shipments',
|
|
195
|
-
readOnlyHint: true,
|
|
196
|
-
destructiveHint: false,
|
|
197
|
-
openWorldHint: true,
|
|
198
|
-
},
|
|
199
|
-
},
|
|
200
|
-
{
|
|
201
|
-
name: 'haruki_search',
|
|
202
|
-
title: 'Global search',
|
|
203
|
-
description: 'Search across products, buyers and registrations. Pass type to narrow the scope.',
|
|
204
|
-
inputSchema: {
|
|
205
|
-
type: 'object',
|
|
206
|
-
properties: {
|
|
207
|
-
q: { type: 'string' },
|
|
208
|
-
type: { type: 'string', enum: ['products', 'buyers', 'registrations'] },
|
|
209
|
-
},
|
|
210
|
-
required: ['q'],
|
|
211
|
-
additionalProperties: false,
|
|
212
|
-
},
|
|
213
|
-
annotations: {
|
|
214
|
-
title: 'Global search',
|
|
215
|
-
readOnlyHint: true,
|
|
216
|
-
destructiveHint: false,
|
|
217
|
-
openWorldHint: true,
|
|
218
|
-
},
|
|
219
|
-
},
|
|
220
|
-
];
|
|
221
|
-
const server = new Server({ name: SERVER_NAME, version: SERVER_VERSION }, { capabilities: { tools: {} } });
|
|
222
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
223
|
-
tools: tools.map((t) => ({ ...t })),
|
|
224
|
-
}));
|
|
80
|
+
let legacyLatched = false;
|
|
81
|
+
/**
|
|
82
|
+
* Requests cancelled via `notifications/cancelled`. The spec requires we send
|
|
83
|
+
* no further messages for them, so the response is dropped when the handler
|
|
84
|
+
* eventually resolves. (The in-flight HTTP call itself still runs to
|
|
85
|
+
* completion — our tools are short single requests, so there is nothing worth
|
|
86
|
+
* tearing down mid-flight.)
|
|
87
|
+
*/
|
|
88
|
+
const cancelled = new Set();
|
|
89
|
+
/**
|
|
90
|
+
* Requests currently being handled.
|
|
91
|
+
*
|
|
92
|
+
* The spec says to exit promptly once stdin closes, but exiting the instant we
|
|
93
|
+
* see EOF would drop replies for anything still in flight — a client that
|
|
94
|
+
* writes a batch of requests and immediately closes stdin would silently lose
|
|
95
|
+
* every answer. So EOF starts a drain instead of an immediate exit.
|
|
96
|
+
*/
|
|
97
|
+
const inFlight = new Set();
|
|
98
|
+
let stdinClosed = false;
|
|
99
|
+
/** How long to keep draining after EOF before giving up on stragglers. */
|
|
100
|
+
const DRAIN_TIMEOUT_MS = 35_000;
|
|
101
|
+
function maybeExit() {
|
|
102
|
+
if (stdinClosed && inFlight.size === 0)
|
|
103
|
+
process.exit(0);
|
|
104
|
+
}
|
|
105
|
+
/* ------------------------------------------------------------- transport */
|
|
106
|
+
function writeMessage(msg) {
|
|
107
|
+
// stdout carries MCP messages only, one per line, no embedded newlines.
|
|
108
|
+
process.stdout.write(`${JSON.stringify(msg)}\n`);
|
|
109
|
+
}
|
|
110
|
+
function sendResult(id, result) {
|
|
111
|
+
if (id !== null && cancelled.has(String(id))) {
|
|
112
|
+
cancelled.delete(String(id));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
writeMessage({ jsonrpc: '2.0', id, result });
|
|
116
|
+
}
|
|
117
|
+
function sendError(id, code, message, data) {
|
|
118
|
+
if (id !== null && cancelled.has(String(id))) {
|
|
119
|
+
cancelled.delete(String(id));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
writeMessage({
|
|
123
|
+
jsonrpc: '2.0',
|
|
124
|
+
id,
|
|
125
|
+
error: data === undefined ? { code, message } : { code, message, data },
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
/* -------------------------------------------------------------- handlers */
|
|
129
|
+
/** `tools/call` — API failures come back as `isError` results, not RPC errors. */
|
|
130
|
+
async function callTool(era, params) {
|
|
131
|
+
const name = typeof params.name === 'string' ? params.name : '';
|
|
132
|
+
const tool = findTool(name);
|
|
133
|
+
if (!tool) {
|
|
134
|
+
return toolResult(era, { success: false, message: `Unknown tool: ${name || '(empty)'}` });
|
|
135
|
+
}
|
|
136
|
+
const args = params.arguments && typeof params.arguments === 'object'
|
|
137
|
+
? params.arguments
|
|
138
|
+
: {};
|
|
139
|
+
try {
|
|
140
|
+
return toolResult(era, await tool.call(client, args));
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
144
|
+
return toolResult(era, { success: false, message: `MCP runtime error: ${msg}` });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
225
147
|
/**
|
|
226
|
-
* MCP
|
|
227
|
-
*
|
|
228
|
-
*
|
|
148
|
+
* MCP requires tool-level failures to surface as `content` + `isError: true`
|
|
149
|
+
* rather than a JSON-RPC error, so the model can see and react to them instead
|
|
150
|
+
* of treating the call as a transport crash.
|
|
229
151
|
*/
|
|
230
|
-
function
|
|
152
|
+
function toolResult(era, result) {
|
|
231
153
|
const r = result;
|
|
232
|
-
const isError = r && typeof r === 'object' && 'success' in r && r.success === false;
|
|
154
|
+
const isError = Boolean(r && typeof r === 'object' && 'success' in r && r.success === false);
|
|
233
155
|
const text = typeof result === 'string' ? result : JSON.stringify(result, null, 2);
|
|
234
|
-
return {
|
|
235
|
-
content: [{ type: 'text', text }],
|
|
236
|
-
...(isError ? { isError: true } : {}),
|
|
237
|
-
};
|
|
156
|
+
return buildResult(era, { content: [{ type: 'text', text }], ...(isError ? { isError: true } : {}) }, SERVER_IDENTITY);
|
|
238
157
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
158
|
+
function listToolsResult(era) {
|
|
159
|
+
return buildCacheableResult(era, { tools: TOOLS.map(toolDescriptor) }, SERVER_IDENTITY, LIST_TTL_MS,
|
|
160
|
+
// The tool list is identical for every token — no user data in it.
|
|
161
|
+
'public');
|
|
162
|
+
}
|
|
163
|
+
function discoverResult() {
|
|
164
|
+
return buildCacheableResult('modern', {
|
|
165
|
+
supportedVersions: SUPPORTED_PROTOCOL_VERSIONS,
|
|
166
|
+
capabilities: { tools: { listChanged: false } },
|
|
167
|
+
instructions: INSTRUCTIONS,
|
|
168
|
+
}, SERVER_IDENTITY, LIST_TTL_MS, 'public');
|
|
169
|
+
}
|
|
170
|
+
/** Route one request. Returns nothing; responses are written as a side effect. */
|
|
171
|
+
async function handleRequest(req) {
|
|
172
|
+
const id = req.id ?? null;
|
|
173
|
+
const era = resolveEra(req, legacyLatched);
|
|
174
|
+
const params = (req.params ?? {});
|
|
175
|
+
if (era === 'modern') {
|
|
176
|
+
// Stateless: validate the declared version on every single request.
|
|
177
|
+
// `server/discover` is exempt — it is precisely the RPC a client uses
|
|
178
|
+
// to find out which versions we accept.
|
|
179
|
+
const declared = getDeclaredProtocolVersion(req);
|
|
180
|
+
if (req.method !== 'server/discover' && declared !== LATEST_PROTOCOL_VERSION) {
|
|
181
|
+
// `supported` lists only the modern revision: retrying a legacy
|
|
182
|
+
// version in modern shape would just fail again. Legacy versions
|
|
183
|
+
// are reachable through `initialize`, and are advertised by
|
|
184
|
+
// `server/discover`.
|
|
185
|
+
sendError(id, UNSUPPORTED_PROTOCOL_VERSION, 'Unsupported protocol version', unsupportedVersionData(declared, [LATEST_PROTOCOL_VERSION]));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
switch (req.method) {
|
|
189
|
+
case 'server/discover':
|
|
190
|
+
sendResult(id, discoverResult());
|
|
191
|
+
return;
|
|
192
|
+
case 'tools/list':
|
|
193
|
+
sendResult(id, listToolsResult(era));
|
|
194
|
+
return;
|
|
195
|
+
case 'tools/call':
|
|
196
|
+
sendResult(id, await callTool(era, params));
|
|
197
|
+
return;
|
|
275
198
|
default:
|
|
276
|
-
|
|
199
|
+
// `initialize`, `ping` and `logging/setLevel` no longer exist in
|
|
200
|
+
// the modern era; they fall through to method-not-found.
|
|
201
|
+
sendError(id, METHOD_NOT_FOUND, `Method not found: ${req.method}`);
|
|
202
|
+
return;
|
|
277
203
|
}
|
|
278
|
-
return toMcpResult(result);
|
|
279
204
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
205
|
+
switch (req.method) {
|
|
206
|
+
case 'initialize': {
|
|
207
|
+
legacyLatched = true;
|
|
208
|
+
const requested = params.protocolVersion;
|
|
209
|
+
const negotiated = negotiateLegacyVersion(requested);
|
|
210
|
+
const info = getClientInfo(req);
|
|
211
|
+
console.error(`[harukibox-mcp] legacy initialize (client=${info ? `${info.name}/${info.version}` : 'unknown'}, ` +
|
|
212
|
+
`requested=${String(requested ?? 'none')}, negotiated=${negotiated})`);
|
|
213
|
+
sendResult(id, {
|
|
214
|
+
protocolVersion: negotiated,
|
|
215
|
+
serverInfo: SERVER_IDENTITY,
|
|
216
|
+
capabilities: { tools: { listChanged: false } },
|
|
217
|
+
instructions: INSTRUCTIONS,
|
|
218
|
+
});
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
case 'ping':
|
|
222
|
+
sendResult(id, {});
|
|
223
|
+
return;
|
|
224
|
+
case 'tools/list':
|
|
225
|
+
sendResult(id, listToolsResult('legacy'));
|
|
226
|
+
return;
|
|
227
|
+
case 'tools/call':
|
|
228
|
+
sendResult(id, await callTool('legacy', params));
|
|
229
|
+
return;
|
|
230
|
+
case 'server/discover':
|
|
231
|
+
// Unreachable — resolveEra always routes discover to the modern
|
|
232
|
+
// branch — but kept explicit so the intent survives refactoring.
|
|
233
|
+
sendResult(id, discoverResult());
|
|
234
|
+
return;
|
|
235
|
+
default:
|
|
236
|
+
sendError(id, METHOD_NOT_FOUND, `Method not found: ${req.method}`);
|
|
237
|
+
return;
|
|
283
238
|
}
|
|
284
|
-
});
|
|
285
|
-
async function main() {
|
|
286
|
-
const transport = new StdioServerTransport();
|
|
287
|
-
await server.connect(transport);
|
|
288
|
-
console.error(`[harukibox-mcp] connected, baseUrl=${baseUrl}`);
|
|
289
239
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
240
|
+
/** Notifications carry no id and MUST NOT be answered. */
|
|
241
|
+
function handleNotification(req) {
|
|
242
|
+
switch (req.method) {
|
|
243
|
+
case 'notifications/initialized':
|
|
244
|
+
legacyLatched = true;
|
|
245
|
+
return;
|
|
246
|
+
case 'notifications/cancelled': {
|
|
247
|
+
const target = req.params?.requestId;
|
|
248
|
+
if (typeof target === 'string' || typeof target === 'number') {
|
|
249
|
+
cancelled.add(String(target));
|
|
250
|
+
}
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
default:
|
|
254
|
+
// Unknown notifications are ignored, per JSON-RPC.
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
function handleLine(line) {
|
|
259
|
+
const trimmed = line.trim();
|
|
260
|
+
if (!trimmed)
|
|
261
|
+
return;
|
|
262
|
+
let msg;
|
|
263
|
+
try {
|
|
264
|
+
msg = JSON.parse(trimmed);
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
sendError(null, PARSE_ERROR, 'Parse error: line is not valid JSON');
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (!msg || typeof msg !== 'object' || msg.jsonrpc !== '2.0' || typeof msg.method !== 'string') {
|
|
271
|
+
sendError(msg?.id ?? null, INVALID_REQUEST, 'Invalid Request');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
// No `id` → notification.
|
|
275
|
+
if (msg.id === undefined || msg.id === null) {
|
|
276
|
+
handleNotification(msg);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const task = handleRequest(msg)
|
|
280
|
+
.catch((err) => {
|
|
281
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
282
|
+
console.error('[harukibox-mcp] handler failed:', err);
|
|
283
|
+
sendError(msg.id ?? null, INTERNAL_ERROR, `Internal error: ${detail}`);
|
|
284
|
+
})
|
|
285
|
+
.finally(() => {
|
|
286
|
+
inFlight.delete(task);
|
|
287
|
+
maybeExit();
|
|
288
|
+
});
|
|
289
|
+
inFlight.add(task);
|
|
290
|
+
}
|
|
291
|
+
function main() {
|
|
292
|
+
const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
|
|
293
|
+
rl.on('line', handleLine);
|
|
294
|
+
// Closing stdin is the portable graceful-shutdown signal. Drain whatever is
|
|
295
|
+
// still in flight first so no reply is lost, then exit.
|
|
296
|
+
rl.on('close', () => {
|
|
297
|
+
stdinClosed = true;
|
|
298
|
+
maybeExit();
|
|
299
|
+
if (inFlight.size > 0) {
|
|
300
|
+
setTimeout(() => {
|
|
301
|
+
console.error(`[harukibox-mcp] drain timed out with ${inFlight.size} request(s) outstanding; exiting`);
|
|
302
|
+
process.exit(0);
|
|
303
|
+
}, DRAIN_TIMEOUT_MS).unref();
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
console.error(`[harukibox-mcp] ready — protocol ${LATEST_PROTOCOL_VERSION} ` +
|
|
307
|
+
`(legacy fallback: ${SUPPORTED_PROTOCOL_VERSIONS.slice(1).join(', ')}), baseUrl=${baseUrl}`);
|
|
308
|
+
}
|
|
309
|
+
main();
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP protocol constants and helpers shared by the stdio server.
|
|
3
|
+
*
|
|
4
|
+
* Targets revision **2026-07-28** while remaining dual-era: the same process
|
|
5
|
+
* still answers the `initialize` handshake used by 2025-11-25 and earlier.
|
|
6
|
+
*
|
|
7
|
+
* Why hand-rolled instead of @modelcontextprotocol/sdk: as of SDK 1.30.0 the
|
|
8
|
+
* TypeScript SDK's LATEST_PROTOCOL_VERSION is still '2025-11-25' and it has no
|
|
9
|
+
* `server/discover`, no `resultType` and no `CacheableResult`. There is no way
|
|
10
|
+
* to emit a 2026-07-28-shaped response through it, so the wire format is
|
|
11
|
+
* produced directly here. Swap back to the SDK once it ships the new revision.
|
|
12
|
+
*
|
|
13
|
+
* Spec: https://modelcontextprotocol.io/specification/2026-07-28
|
|
14
|
+
*/
|
|
15
|
+
/** The modern (stateless, per-request metadata) revision this server speaks. */
|
|
16
|
+
export const LATEST_PROTOCOL_VERSION = '2026-07-28';
|
|
17
|
+
/**
|
|
18
|
+
* Legacy revisions still answered through the `initialize` handshake.
|
|
19
|
+
* Ordered newest-first — the first entry is what we negotiate down to when a
|
|
20
|
+
* legacy client asks for something we don't recognise.
|
|
21
|
+
*/
|
|
22
|
+
export const LEGACY_PROTOCOL_VERSIONS = ['2025-11-25', '2025-06-18', '2025-03-26'];
|
|
23
|
+
/** Everything this server can speak, newest first. Advertised by server/discover. */
|
|
24
|
+
export const SUPPORTED_PROTOCOL_VERSIONS = [
|
|
25
|
+
LATEST_PROTOCOL_VERSION,
|
|
26
|
+
...LEGACY_PROTOCOL_VERSIONS,
|
|
27
|
+
];
|
|
28
|
+
/* ------------------------------------------------------------------ _meta */
|
|
29
|
+
export const META_PROTOCOL_VERSION = 'io.modelcontextprotocol/protocolVersion';
|
|
30
|
+
export const META_CLIENT_INFO = 'io.modelcontextprotocol/clientInfo';
|
|
31
|
+
export const META_CLIENT_CAPABILITIES = 'io.modelcontextprotocol/clientCapabilities';
|
|
32
|
+
export const META_SERVER_INFO = 'io.modelcontextprotocol/serverInfo';
|
|
33
|
+
/* ---------------------------------------------------------------- errors */
|
|
34
|
+
/** JSON-RPC standard codes. */
|
|
35
|
+
export const PARSE_ERROR = -32700;
|
|
36
|
+
export const INVALID_REQUEST = -32600;
|
|
37
|
+
export const METHOD_NOT_FOUND = -32601;
|
|
38
|
+
export const INVALID_PARAMS = -32602;
|
|
39
|
+
export const INTERNAL_ERROR = -32603;
|
|
40
|
+
/**
|
|
41
|
+
* MCP-allocated codes (2026-07-28 reserves -32020..-32099 for the spec;
|
|
42
|
+
* -32000..-32019 stays implementation-defined).
|
|
43
|
+
*/
|
|
44
|
+
export const HEADER_MISMATCH = -32020;
|
|
45
|
+
export const MISSING_REQUIRED_CLIENT_CAPABILITY = -32021;
|
|
46
|
+
export const UNSUPPORTED_PROTOCOL_VERSION = -32022;
|
|
47
|
+
/* --------------------------------------------------------------- helpers */
|
|
48
|
+
/** Read the request's `_meta` object, if any. */
|
|
49
|
+
export function getRequestMeta(req) {
|
|
50
|
+
const meta = req.params?._meta;
|
|
51
|
+
return meta && typeof meta === 'object' ? meta : {};
|
|
52
|
+
}
|
|
53
|
+
/** The protocol version declared in `_meta`, or undefined for legacy requests. */
|
|
54
|
+
export function getDeclaredProtocolVersion(req) {
|
|
55
|
+
const v = getRequestMeta(req)[META_PROTOCOL_VERSION];
|
|
56
|
+
return typeof v === 'string' && v.length > 0 ? v : undefined;
|
|
57
|
+
}
|
|
58
|
+
/** Self-reported client identity. Display/logging only — never a security input. */
|
|
59
|
+
export function getClientInfo(req) {
|
|
60
|
+
const v = getRequestMeta(req)[META_CLIENT_INFO];
|
|
61
|
+
if (!v || typeof v !== 'object')
|
|
62
|
+
return undefined;
|
|
63
|
+
const o = v;
|
|
64
|
+
if (typeof o.name !== 'string' || typeof o.version !== 'string')
|
|
65
|
+
return undefined;
|
|
66
|
+
return { name: o.name, version: o.version };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Decide which era a request belongs to.
|
|
70
|
+
*
|
|
71
|
+
* Per spec §Versioning, a dual-era server picks behaviour from how the client
|
|
72
|
+
* opens: a request carrying modern `_meta` is served statelessly, while an
|
|
73
|
+
* `initialize` request selects legacy semantics for the rest of the process.
|
|
74
|
+
*
|
|
75
|
+
* `server/discover` is modern-only — it is also the documented stdio probe a
|
|
76
|
+
* dual-era *client* uses to detect our era, so it must always answer modern
|
|
77
|
+
* even when the client sent no `_meta` at all.
|
|
78
|
+
*/
|
|
79
|
+
export function resolveEra(req, legacyLatched) {
|
|
80
|
+
if (req.method === 'server/discover')
|
|
81
|
+
return 'modern';
|
|
82
|
+
if (getDeclaredProtocolVersion(req) !== undefined)
|
|
83
|
+
return 'modern';
|
|
84
|
+
if (legacyLatched)
|
|
85
|
+
return 'legacy';
|
|
86
|
+
// No `_meta`, no prior `initialize`: an older client that skipped the
|
|
87
|
+
// handshake. Serving it legacy-shaped is the lenient, compatible choice.
|
|
88
|
+
return 'legacy';
|
|
89
|
+
}
|
|
90
|
+
/** Negotiate a legacy `initialize`: echo the client's version if we know it. */
|
|
91
|
+
export function negotiateLegacyVersion(requested) {
|
|
92
|
+
if (typeof requested === 'string' && LEGACY_PROTOCOL_VERSIONS.includes(requested)) {
|
|
93
|
+
return requested;
|
|
94
|
+
}
|
|
95
|
+
return LEGACY_PROTOCOL_VERSIONS[0];
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Wrap a result payload for the wire.
|
|
99
|
+
*
|
|
100
|
+
* Modern results MUST carry `resultType` and SHOULD carry `serverInfo` in
|
|
101
|
+
* `_meta`. Legacy results must carry neither — older clients validate against
|
|
102
|
+
* their own schema, where those fields are unknown.
|
|
103
|
+
*/
|
|
104
|
+
export function buildResult(era, payload, server) {
|
|
105
|
+
if (era === 'legacy')
|
|
106
|
+
return payload;
|
|
107
|
+
return {
|
|
108
|
+
...payload,
|
|
109
|
+
resultType: 'complete',
|
|
110
|
+
_meta: { [META_SERVER_INFO]: { name: server.name, version: server.version } },
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Wrap a list-style result, which in 2026-07-28 additionally MUST carry the
|
|
115
|
+
* `CacheableResult` fields.
|
|
116
|
+
*
|
|
117
|
+
* @param ttlMs how long the client may cache this, in ms.
|
|
118
|
+
* @param cacheScope 'public' when the payload holds no user-specific data and
|
|
119
|
+
* shared intermediaries may reuse it across auth contexts.
|
|
120
|
+
*/
|
|
121
|
+
export function buildCacheableResult(era, payload, server, ttlMs, cacheScope) {
|
|
122
|
+
if (era === 'legacy')
|
|
123
|
+
return payload;
|
|
124
|
+
return { ...buildResult(era, payload, server), ttlMs, cacheScope };
|
|
125
|
+
}
|
|
126
|
+
/** `UnsupportedProtocolVersionError` data payload (-32022). */
|
|
127
|
+
export function unsupportedVersionData(requested, supported) {
|
|
128
|
+
return { supported, requested: requested ?? null };
|
|
129
|
+
}
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* harukibox MCP tool definitions.
|
|
3
|
+
*
|
|
4
|
+
* Kept deliberately in the same order as the array literal: 2026-07-28 asks
|
|
5
|
+
* servers to return `tools/list` in a deterministic order so clients can cache
|
|
6
|
+
* the list and LLM prompt caches keep hitting.
|
|
7
|
+
*
|
|
8
|
+
* Annotations follow the spec's `ToolAnnotations`:
|
|
9
|
+
* - readOnlyHint — no observable state mutation.
|
|
10
|
+
* - destructiveHint — can permanently change data the user cares about.
|
|
11
|
+
* - openWorldHint — reaches beyond the local process (our HTTP API).
|
|
12
|
+
*/
|
|
13
|
+
function requireString(args, key) {
|
|
14
|
+
const v = args[key];
|
|
15
|
+
if (typeof v !== 'string' || v.length === 0)
|
|
16
|
+
return { error: `${key} is required (non-empty string)` };
|
|
17
|
+
return v;
|
|
18
|
+
}
|
|
19
|
+
export const TOOLS = [
|
|
20
|
+
{
|
|
21
|
+
name: 'haruki_me',
|
|
22
|
+
title: 'Get account info',
|
|
23
|
+
description: 'Return the user, organization and token info bound to this MCP connection. Always call first to confirm which harukibox store the agent is operating against.',
|
|
24
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
25
|
+
annotations: { title: 'Get account info', readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
26
|
+
call: (client) => client.me(),
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'haruki_list_products',
|
|
30
|
+
title: 'List products',
|
|
31
|
+
description: 'List products in the connected harukibox store. Supports search (matches name + SKU), status filter and pagination.',
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
page: { type: 'integer', minimum: 1, default: 1 },
|
|
36
|
+
limit: { type: 'integer', minimum: 1, maximum: 100, default: 20 },
|
|
37
|
+
search: { type: 'string', description: 'Match against name and SKU.' },
|
|
38
|
+
status: { type: 'string', description: '現貨 / 預購 / 售罄 / active 等' },
|
|
39
|
+
},
|
|
40
|
+
additionalProperties: false,
|
|
41
|
+
},
|
|
42
|
+
annotations: { title: 'List products', readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
43
|
+
call: (client, a) => client.listProducts(a),
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: 'haruki_get_product',
|
|
47
|
+
title: 'Get product',
|
|
48
|
+
description: 'Fetch a single product by its UUID. Returns 404 if the product is not in the calling organization.',
|
|
49
|
+
inputSchema: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: { id: { type: 'string', description: 'Product UUID' } },
|
|
52
|
+
required: ['id'],
|
|
53
|
+
additionalProperties: false,
|
|
54
|
+
},
|
|
55
|
+
annotations: { title: 'Get product', readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
56
|
+
call: async (client, a) => {
|
|
57
|
+
const id = requireString(a, 'id');
|
|
58
|
+
if (typeof id !== 'string')
|
|
59
|
+
return { success: false, message: id.error };
|
|
60
|
+
return client.getProduct(id);
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'haruki_create_product',
|
|
65
|
+
title: 'Create product',
|
|
66
|
+
description: 'Create a new product. Requires the products:write scope. Will fail with QUOTA_EXCEEDED if the organization plan limit is reached.',
|
|
67
|
+
inputSchema: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
name: { type: 'string' },
|
|
71
|
+
sku: { type: 'string' },
|
|
72
|
+
priceTwd: { type: 'number' },
|
|
73
|
+
priceJpy: { type: 'number' },
|
|
74
|
+
description: { type: 'string' },
|
|
75
|
+
category: { type: 'string' },
|
|
76
|
+
status: { type: 'string' },
|
|
77
|
+
initialStock: { type: 'integer' },
|
|
78
|
+
releaseDate: { type: 'string' },
|
|
79
|
+
ipId: { type: 'string' },
|
|
80
|
+
locationId: { type: 'string' },
|
|
81
|
+
},
|
|
82
|
+
required: ['name', 'sku'],
|
|
83
|
+
additionalProperties: false,
|
|
84
|
+
},
|
|
85
|
+
// Creating is a write but not destructive — nothing existing is lost.
|
|
86
|
+
annotations: { title: 'Create product', readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
87
|
+
call: (client, a) => client.createProduct(a),
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'haruki_list_registrations',
|
|
91
|
+
title: 'List registrations (orders)',
|
|
92
|
+
description: 'List orders (registrations) for the connected store. Filter by status or buyer.',
|
|
93
|
+
inputSchema: {
|
|
94
|
+
type: 'object',
|
|
95
|
+
properties: {
|
|
96
|
+
page: { type: 'integer', minimum: 1, default: 1 },
|
|
97
|
+
limit: { type: 'integer', minimum: 1, maximum: 100, default: 20 },
|
|
98
|
+
status: { type: 'string' },
|
|
99
|
+
buyerId: { type: 'string' },
|
|
100
|
+
},
|
|
101
|
+
additionalProperties: false,
|
|
102
|
+
},
|
|
103
|
+
annotations: { title: 'List registrations', readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
104
|
+
call: (client, a) => client.listRegistrations(a),
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: 'haruki_list_buyers',
|
|
108
|
+
title: 'List buyers',
|
|
109
|
+
description: 'List buyers in the connected store. Search matches across name / phone / email.',
|
|
110
|
+
inputSchema: {
|
|
111
|
+
type: 'object',
|
|
112
|
+
properties: {
|
|
113
|
+
page: { type: 'integer', minimum: 1, default: 1 },
|
|
114
|
+
limit: { type: 'integer', minimum: 1, maximum: 100, default: 20 },
|
|
115
|
+
search: { type: 'string' },
|
|
116
|
+
},
|
|
117
|
+
additionalProperties: false,
|
|
118
|
+
},
|
|
119
|
+
annotations: { title: 'List buyers', readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
120
|
+
call: (client, a) => client.listBuyers(a),
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'haruki_list_shippings',
|
|
124
|
+
title: 'List shipments',
|
|
125
|
+
description: 'List shipments. Filter by status if provided.',
|
|
126
|
+
inputSchema: {
|
|
127
|
+
type: 'object',
|
|
128
|
+
properties: {
|
|
129
|
+
page: { type: 'integer', minimum: 1, default: 1 },
|
|
130
|
+
limit: { type: 'integer', minimum: 1, maximum: 100, default: 20 },
|
|
131
|
+
status: { type: 'string' },
|
|
132
|
+
},
|
|
133
|
+
additionalProperties: false,
|
|
134
|
+
},
|
|
135
|
+
annotations: { title: 'List shipments', readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
136
|
+
call: (client, a) => client.listShippings(a),
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'haruki_search',
|
|
140
|
+
title: 'Global search',
|
|
141
|
+
description: 'Search across products, buyers and registrations. Pass type to narrow the scope.',
|
|
142
|
+
inputSchema: {
|
|
143
|
+
type: 'object',
|
|
144
|
+
properties: {
|
|
145
|
+
q: { type: 'string' },
|
|
146
|
+
type: { type: 'string', enum: ['products', 'buyers', 'registrations'] },
|
|
147
|
+
},
|
|
148
|
+
required: ['q'],
|
|
149
|
+
additionalProperties: false,
|
|
150
|
+
},
|
|
151
|
+
annotations: { title: 'Global search', readOnlyHint: true, destructiveHint: false, openWorldHint: true },
|
|
152
|
+
call: async (client, a) => {
|
|
153
|
+
const q = requireString(a, 'q');
|
|
154
|
+
if (typeof q !== 'string')
|
|
155
|
+
return { success: false, message: q.error };
|
|
156
|
+
return client.search(q, a.type);
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
export function findTool(name) {
|
|
161
|
+
return TOOLS.find((t) => t.name === name);
|
|
162
|
+
}
|
|
163
|
+
/** The wire shape of a tool, as returned by `tools/list`. */
|
|
164
|
+
export function toolDescriptor(t) {
|
|
165
|
+
return {
|
|
166
|
+
name: t.name,
|
|
167
|
+
title: t.title,
|
|
168
|
+
description: t.description,
|
|
169
|
+
inputSchema: t.inputSchema,
|
|
170
|
+
annotations: t.annotations,
|
|
171
|
+
};
|
|
172
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@harukibox/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "Model Context Protocol server for harukibox — let your AI agent manage your inventory, orders, buyers and shipments through the official harukibox API.",
|
|
5
|
+
"description": "Model Context Protocol server for harukibox (MCP 2026-07-28, dual-era) — let your AI agent manage your inventory, orders, buyers and shipments through the official harukibox API.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"bin": {
|
|
@@ -18,9 +18,6 @@
|
|
|
18
18
|
"prepack": "npm run build",
|
|
19
19
|
"start": "node ./bin/harukibox-mcp.js"
|
|
20
20
|
},
|
|
21
|
-
"dependencies": {
|
|
22
|
-
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
23
|
-
},
|
|
24
21
|
"devDependencies": {
|
|
25
22
|
"@types/node": "^20.0.0",
|
|
26
23
|
"typescript": "^5.0.0"
|
|
@@ -28,9 +25,14 @@
|
|
|
28
25
|
"engines": {
|
|
29
26
|
"node": ">=18"
|
|
30
27
|
},
|
|
31
|
-
"homepage": "https://
|
|
28
|
+
"homepage": "https://github.com/cosmopig/harukibox-agent#readme",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/cosmopig/harukibox-agent/issues"
|
|
31
|
+
},
|
|
32
32
|
"repository": {
|
|
33
33
|
"type": "git",
|
|
34
|
-
"url": "https://github.com/cosmopig/
|
|
35
|
-
|
|
34
|
+
"url": "https://github.com/cosmopig/harukibox-agent.git",
|
|
35
|
+
"directory": "packages/mcp"
|
|
36
|
+
},
|
|
37
|
+
"keywords": ["harukibox", "mcp", "model-context-protocol", "mcp-2026-07-28", "claude", "agent", "japan-import", "代購"]
|
|
36
38
|
}
|