apick-mcp 3.4.1 → 3.5.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/CHANGELOG.md +7 -0
- package/LICENSE +21 -21
- package/README.md +52 -13
- package/TOOLS.md +2283 -2071
- package/package.json +2 -2
- package/src/bridge.js +175 -175
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apick-mcp",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "APICK MCP —
|
|
3
|
+
"version": "3.5.0",
|
|
4
|
+
"description": "APICK MCP — 106 Korean data, AI, image & video tools for AI agents. 사업자조회·신분증·OCR·배송조회·이미지 생성·편집·영상 생성·파일변환·검색·LLM.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "APICK",
|
package/src/bridge.js
CHANGED
|
@@ -1,175 +1,175 @@
|
|
|
1
|
-
// stdio <-> Streamable HTTP bridge for the APICK MCP server.
|
|
2
|
-
//
|
|
3
|
-
// APICK serves MCP over Streamable HTTP at https://apick.app/mcp/{server}. Clients that
|
|
4
|
-
// speak remote HTTP should connect there directly. This bridge exists for clients that
|
|
5
|
-
// only launch local stdio processes.
|
|
6
|
-
//
|
|
7
|
-
// It is a pump, not a protocol implementation: every JSON-RPC message read from stdin is
|
|
8
|
-
// forwarded verbatim, and every message the server returns is written verbatim to stdout.
|
|
9
|
-
// Capability and version negotiation, tool schemas and errors are all handled end to end
|
|
10
|
-
// by the client and the server.
|
|
11
|
-
|
|
12
|
-
export const DEFAULT_ENDPOINT = 'https://apick.app/mcp';
|
|
13
|
-
|
|
14
|
-
export const SERVERS = [
|
|
15
|
-
'all',
|
|
16
|
-
'business',
|
|
17
|
-
'identity',
|
|
18
|
-
'ocr',
|
|
19
|
-
'finance',
|
|
20
|
-
'web',
|
|
21
|
-
'convert',
|
|
22
|
-
'vision',
|
|
23
|
-
'ai'
|
|
24
|
-
];
|
|
25
|
-
|
|
26
|
-
// A key must never reach stdout, stderr or an error message.
|
|
27
|
-
export function redact(text, apiKey) {
|
|
28
|
-
if (!text) return text;
|
|
29
|
-
let out = String(text);
|
|
30
|
-
if (apiKey) out = out.split(apiKey).join('***');
|
|
31
|
-
return out.replace(/(Bearer\s+)[\w.\-]+/gi, '$1***');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Streamable HTTP replies with either a single JSON body or an SSE stream carrying one or
|
|
35
|
-
// more JSON-RPC messages. Both shapes reduce to "a list of messages".
|
|
36
|
-
function parseEventStream(text) {
|
|
37
|
-
const messages = [];
|
|
38
|
-
for (const block of text.split(/\r?\n\r?\n/)) {
|
|
39
|
-
const data = block
|
|
40
|
-
.split(/\r?\n/)
|
|
41
|
-
.filter((line) => line.startsWith('data:'))
|
|
42
|
-
.map((line) => line.slice(5).trim())
|
|
43
|
-
.join('\n');
|
|
44
|
-
if (!data) continue;
|
|
45
|
-
try {
|
|
46
|
-
messages.push(JSON.parse(data));
|
|
47
|
-
} catch {
|
|
48
|
-
// Keep-alive or comment frame - nothing to forward.
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return messages;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function createBridge(options = {}) {
|
|
55
|
-
const server = options.server || 'all';
|
|
56
|
-
const apiKey = options.apiKey || '';
|
|
57
|
-
const endpoint = (options.endpoint || DEFAULT_ENDPOINT).replace(/\/+$/, '') + '/' + server;
|
|
58
|
-
const fetchImpl = options.fetch || globalThis.fetch;
|
|
59
|
-
const write = options.write || ((line) => process.stdout.write(line + '\n'));
|
|
60
|
-
const warn = options.warn || ((line) => process.stderr.write(line + '\n'));
|
|
61
|
-
|
|
62
|
-
// The gateway is stateless, but honour a session id and the negotiated protocol
|
|
63
|
-
// version if the server ever starts sending them.
|
|
64
|
-
let sessionId = null;
|
|
65
|
-
let protocolVersion = null;
|
|
66
|
-
|
|
67
|
-
function headers() {
|
|
68
|
-
const h = {
|
|
69
|
-
'content-type': 'application/json',
|
|
70
|
-
accept: 'application/json, text/event-stream',
|
|
71
|
-
'user-agent': 'apick-mcp-bridge'
|
|
72
|
-
};
|
|
73
|
-
if (apiKey) h.authorization = 'Bearer ' + apiKey;
|
|
74
|
-
if (sessionId) h['mcp-session-id'] = sessionId;
|
|
75
|
-
if (protocolVersion) h['mcp-protocol-version'] = protocolVersion;
|
|
76
|
-
return h;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function remember(message) {
|
|
80
|
-
const negotiated = message && message.result && message.result.protocolVersion;
|
|
81
|
-
if (negotiated) protocolVersion = negotiated;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async function send(message) {
|
|
85
|
-
let res;
|
|
86
|
-
try {
|
|
87
|
-
res = await fetchImpl(endpoint, {
|
|
88
|
-
method: 'POST',
|
|
89
|
-
headers: headers(),
|
|
90
|
-
body: JSON.stringify(message)
|
|
91
|
-
});
|
|
92
|
-
} catch (err) {
|
|
93
|
-
return fail(message, -32001, 'Cannot reach ' + endpoint + ': ' + redact(err.message, apiKey));
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const incomingSession = res.headers.get && res.headers.get('mcp-session-id');
|
|
97
|
-
if (incomingSession) sessionId = incomingSession;
|
|
98
|
-
|
|
99
|
-
const body = await res.text();
|
|
100
|
-
if (!body) {
|
|
101
|
-
// 202 for notifications and responses - nothing to forward.
|
|
102
|
-
if (res.ok) return;
|
|
103
|
-
return fail(message, -32002, 'HTTP ' + res.status + ' from ' + endpoint);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const contentType = (res.headers.get && res.headers.get('content-type')) || '';
|
|
107
|
-
let messages;
|
|
108
|
-
if (contentType.includes('text/event-stream')) {
|
|
109
|
-
messages = parseEventStream(body);
|
|
110
|
-
} else {
|
|
111
|
-
try {
|
|
112
|
-
const parsed = JSON.parse(body);
|
|
113
|
-
messages = Array.isArray(parsed) ? parsed : [parsed];
|
|
114
|
-
} catch {
|
|
115
|
-
return fail(message, -32002, 'HTTP ' + res.status + ' from ' + endpoint + ': unexpected response body');
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
for (const out of messages) {
|
|
120
|
-
remember(out);
|
|
121
|
-
write(JSON.stringify(out));
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Only a request (one carrying an id) can be answered with an error; a failed
|
|
126
|
-
// notification is reported on stderr so the client is not left waiting on a reply.
|
|
127
|
-
function fail(message, code, text) {
|
|
128
|
-
const detail = redact(text, apiKey);
|
|
129
|
-
if (message && message.id !== undefined && message.id !== null) {
|
|
130
|
-
write(JSON.stringify({ jsonrpc: '2.0', id: message.id, error: { code, message: detail } }));
|
|
131
|
-
} else {
|
|
132
|
-
warn('apick-mcp: ' + detail);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Newline-delimited JSON in, newline-delimited JSON out.
|
|
137
|
-
function handleLine(line) {
|
|
138
|
-
const trimmed = line.trim();
|
|
139
|
-
if (!trimmed) return Promise.resolve();
|
|
140
|
-
let message;
|
|
141
|
-
try {
|
|
142
|
-
message = JSON.parse(trimmed);
|
|
143
|
-
} catch {
|
|
144
|
-
warn('apick-mcp: ignoring unparseable input line');
|
|
145
|
-
return Promise.resolve();
|
|
146
|
-
}
|
|
147
|
-
return send(message);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function attach(input) {
|
|
151
|
-
let buffer = '';
|
|
152
|
-
const pending = new Set();
|
|
153
|
-
input.setEncoding('utf8');
|
|
154
|
-
input.on('data', (chunk) => {
|
|
155
|
-
buffer += chunk;
|
|
156
|
-
let index;
|
|
157
|
-
while ((index = buffer.indexOf('\n')) !== -1) {
|
|
158
|
-
const line = buffer.slice(0, index);
|
|
159
|
-
buffer = buffer.slice(index + 1);
|
|
160
|
-
const task = handleLine(line).catch((err) => warn('apick-mcp: ' + redact(err.message, apiKey)));
|
|
161
|
-
pending.add(task);
|
|
162
|
-
task.finally(() => pending.delete(task));
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
return new Promise((resolve) => {
|
|
166
|
-
input.on('end', async () => {
|
|
167
|
-
if (buffer.trim()) await handleLine(buffer).catch(() => {});
|
|
168
|
-
await Promise.allSettled([...pending]);
|
|
169
|
-
resolve();
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return { endpoint, attach, handleLine };
|
|
175
|
-
}
|
|
1
|
+
// stdio <-> Streamable HTTP bridge for the APICK MCP server.
|
|
2
|
+
//
|
|
3
|
+
// APICK serves MCP over Streamable HTTP at https://apick.app/mcp/{server}. Clients that
|
|
4
|
+
// speak remote HTTP should connect there directly. This bridge exists for clients that
|
|
5
|
+
// only launch local stdio processes.
|
|
6
|
+
//
|
|
7
|
+
// It is a pump, not a protocol implementation: every JSON-RPC message read from stdin is
|
|
8
|
+
// forwarded verbatim, and every message the server returns is written verbatim to stdout.
|
|
9
|
+
// Capability and version negotiation, tool schemas and errors are all handled end to end
|
|
10
|
+
// by the client and the server.
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_ENDPOINT = 'https://apick.app/mcp';
|
|
13
|
+
|
|
14
|
+
export const SERVERS = [
|
|
15
|
+
'all',
|
|
16
|
+
'business',
|
|
17
|
+
'identity',
|
|
18
|
+
'ocr',
|
|
19
|
+
'finance',
|
|
20
|
+
'web',
|
|
21
|
+
'convert',
|
|
22
|
+
'vision',
|
|
23
|
+
'ai'
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
// A key must never reach stdout, stderr or an error message.
|
|
27
|
+
export function redact(text, apiKey) {
|
|
28
|
+
if (!text) return text;
|
|
29
|
+
let out = String(text);
|
|
30
|
+
if (apiKey) out = out.split(apiKey).join('***');
|
|
31
|
+
return out.replace(/(Bearer\s+)[\w.\-]+/gi, '$1***');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Streamable HTTP replies with either a single JSON body or an SSE stream carrying one or
|
|
35
|
+
// more JSON-RPC messages. Both shapes reduce to "a list of messages".
|
|
36
|
+
function parseEventStream(text) {
|
|
37
|
+
const messages = [];
|
|
38
|
+
for (const block of text.split(/\r?\n\r?\n/)) {
|
|
39
|
+
const data = block
|
|
40
|
+
.split(/\r?\n/)
|
|
41
|
+
.filter((line) => line.startsWith('data:'))
|
|
42
|
+
.map((line) => line.slice(5).trim())
|
|
43
|
+
.join('\n');
|
|
44
|
+
if (!data) continue;
|
|
45
|
+
try {
|
|
46
|
+
messages.push(JSON.parse(data));
|
|
47
|
+
} catch {
|
|
48
|
+
// Keep-alive or comment frame - nothing to forward.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return messages;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function createBridge(options = {}) {
|
|
55
|
+
const server = options.server || 'all';
|
|
56
|
+
const apiKey = options.apiKey || '';
|
|
57
|
+
const endpoint = (options.endpoint || DEFAULT_ENDPOINT).replace(/\/+$/, '') + '/' + server;
|
|
58
|
+
const fetchImpl = options.fetch || globalThis.fetch;
|
|
59
|
+
const write = options.write || ((line) => process.stdout.write(line + '\n'));
|
|
60
|
+
const warn = options.warn || ((line) => process.stderr.write(line + '\n'));
|
|
61
|
+
|
|
62
|
+
// The gateway is stateless, but honour a session id and the negotiated protocol
|
|
63
|
+
// version if the server ever starts sending them.
|
|
64
|
+
let sessionId = null;
|
|
65
|
+
let protocolVersion = null;
|
|
66
|
+
|
|
67
|
+
function headers() {
|
|
68
|
+
const h = {
|
|
69
|
+
'content-type': 'application/json',
|
|
70
|
+
accept: 'application/json, text/event-stream',
|
|
71
|
+
'user-agent': 'apick-mcp-bridge'
|
|
72
|
+
};
|
|
73
|
+
if (apiKey) h.authorization = 'Bearer ' + apiKey;
|
|
74
|
+
if (sessionId) h['mcp-session-id'] = sessionId;
|
|
75
|
+
if (protocolVersion) h['mcp-protocol-version'] = protocolVersion;
|
|
76
|
+
return h;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function remember(message) {
|
|
80
|
+
const negotiated = message && message.result && message.result.protocolVersion;
|
|
81
|
+
if (negotiated) protocolVersion = negotiated;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function send(message) {
|
|
85
|
+
let res;
|
|
86
|
+
try {
|
|
87
|
+
res = await fetchImpl(endpoint, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers: headers(),
|
|
90
|
+
body: JSON.stringify(message)
|
|
91
|
+
});
|
|
92
|
+
} catch (err) {
|
|
93
|
+
return fail(message, -32001, 'Cannot reach ' + endpoint + ': ' + redact(err.message, apiKey));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const incomingSession = res.headers.get && res.headers.get('mcp-session-id');
|
|
97
|
+
if (incomingSession) sessionId = incomingSession;
|
|
98
|
+
|
|
99
|
+
const body = await res.text();
|
|
100
|
+
if (!body) {
|
|
101
|
+
// 202 for notifications and responses - nothing to forward.
|
|
102
|
+
if (res.ok) return;
|
|
103
|
+
return fail(message, -32002, 'HTTP ' + res.status + ' from ' + endpoint);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const contentType = (res.headers.get && res.headers.get('content-type')) || '';
|
|
107
|
+
let messages;
|
|
108
|
+
if (contentType.includes('text/event-stream')) {
|
|
109
|
+
messages = parseEventStream(body);
|
|
110
|
+
} else {
|
|
111
|
+
try {
|
|
112
|
+
const parsed = JSON.parse(body);
|
|
113
|
+
messages = Array.isArray(parsed) ? parsed : [parsed];
|
|
114
|
+
} catch {
|
|
115
|
+
return fail(message, -32002, 'HTTP ' + res.status + ' from ' + endpoint + ': unexpected response body');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
for (const out of messages) {
|
|
120
|
+
remember(out);
|
|
121
|
+
write(JSON.stringify(out));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Only a request (one carrying an id) can be answered with an error; a failed
|
|
126
|
+
// notification is reported on stderr so the client is not left waiting on a reply.
|
|
127
|
+
function fail(message, code, text) {
|
|
128
|
+
const detail = redact(text, apiKey);
|
|
129
|
+
if (message && message.id !== undefined && message.id !== null) {
|
|
130
|
+
write(JSON.stringify({ jsonrpc: '2.0', id: message.id, error: { code, message: detail } }));
|
|
131
|
+
} else {
|
|
132
|
+
warn('apick-mcp: ' + detail);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Newline-delimited JSON in, newline-delimited JSON out.
|
|
137
|
+
function handleLine(line) {
|
|
138
|
+
const trimmed = line.trim();
|
|
139
|
+
if (!trimmed) return Promise.resolve();
|
|
140
|
+
let message;
|
|
141
|
+
try {
|
|
142
|
+
message = JSON.parse(trimmed);
|
|
143
|
+
} catch {
|
|
144
|
+
warn('apick-mcp: ignoring unparseable input line');
|
|
145
|
+
return Promise.resolve();
|
|
146
|
+
}
|
|
147
|
+
return send(message);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function attach(input) {
|
|
151
|
+
let buffer = '';
|
|
152
|
+
const pending = new Set();
|
|
153
|
+
input.setEncoding('utf8');
|
|
154
|
+
input.on('data', (chunk) => {
|
|
155
|
+
buffer += chunk;
|
|
156
|
+
let index;
|
|
157
|
+
while ((index = buffer.indexOf('\n')) !== -1) {
|
|
158
|
+
const line = buffer.slice(0, index);
|
|
159
|
+
buffer = buffer.slice(index + 1);
|
|
160
|
+
const task = handleLine(line).catch((err) => warn('apick-mcp: ' + redact(err.message, apiKey)));
|
|
161
|
+
pending.add(task);
|
|
162
|
+
task.finally(() => pending.delete(task));
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
return new Promise((resolve) => {
|
|
166
|
+
input.on('end', async () => {
|
|
167
|
+
if (buffer.trim()) await handleLine(buffer).catch(() => {});
|
|
168
|
+
await Promise.allSettled([...pending]);
|
|
169
|
+
resolve();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { endpoint, attach, handleLine };
|
|
175
|
+
}
|