@1key4ai/mcp-studio 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/dist/index.d.ts +11 -0
- package/dist/index.js +638 -0
- package/package.json +45 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @1key4ai/mcp-studio — 1K4 Studio bridge MCP server for Claude Code.
|
|
4
|
+
*
|
|
5
|
+
* Exposes tools for interacting with 1K4 Studio agent sessions:
|
|
6
|
+
* send messages, wait for user input, manage files, answer questionnaires.
|
|
7
|
+
*
|
|
8
|
+
* Split from @1key4ai/mcp-review in v0.1.7 so review and studio tools
|
|
9
|
+
* live in separate MCP servers (1k4-review vs 1k4-studio).
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @1key4ai/mcp-studio — 1K4 Studio bridge MCP server for Claude Code.
|
|
4
|
+
*
|
|
5
|
+
* Exposes tools for interacting with 1K4 Studio agent sessions:
|
|
6
|
+
* send messages, wait for user input, manage files, answer questionnaires.
|
|
7
|
+
*
|
|
8
|
+
* Split from @1key4ai/mcp-review in v0.1.7 so review and studio tools
|
|
9
|
+
* live in separate MCP servers (1k4-review vs 1k4-studio).
|
|
10
|
+
*/
|
|
11
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
12
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
14
|
+
const API_BASE = process.env.ONEKEY_API_BASE || "https://1key4ai.com";
|
|
15
|
+
const API_KEY = process.env.ONEKEY_API_KEY || "";
|
|
16
|
+
if (!API_KEY) {
|
|
17
|
+
console.error("Error: ONEKEY_API_KEY environment variable is required.\n" +
|
|
18
|
+
"Run: npx -y -p @1key4ai/mcp-review mcp-review setup --api-key sk-xxx");
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// API helpers
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
async function fetchWithRetry(url, options, maxRetries = 3) {
|
|
25
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
26
|
+
try {
|
|
27
|
+
const res = await fetch(url, options);
|
|
28
|
+
if (res.status === 429) {
|
|
29
|
+
if (attempt < maxRetries) {
|
|
30
|
+
const retryAfter = parseInt(res.headers.get("Retry-After") || "5", 10);
|
|
31
|
+
console.error(`Rate limited, waiting ${retryAfter}s (${attempt + 1}/${maxRetries})`);
|
|
32
|
+
await new Promise(r => setTimeout(r, retryAfter * 1000));
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
throw new Error(`Rate limited after ${maxRetries} retries. Try again later.`);
|
|
36
|
+
}
|
|
37
|
+
if (res.status >= 500) {
|
|
38
|
+
if (attempt < maxRetries) {
|
|
39
|
+
console.error(`Server error ${res.status}, retrying (${attempt + 1}/${maxRetries})`);
|
|
40
|
+
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt)));
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`Server error ${res.status} after ${maxRetries} retries.`);
|
|
44
|
+
}
|
|
45
|
+
return res;
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
if (attempt === maxRetries)
|
|
49
|
+
throw err;
|
|
50
|
+
console.error(`Network error, retrying (${attempt + 1}/${maxRetries}): ${err}`);
|
|
51
|
+
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, attempt)));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
throw new Error("Unreachable");
|
|
55
|
+
}
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Bridge API helpers
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
async function sendMessage(params) {
|
|
60
|
+
const res = await fetchWithRetry(`${API_BASE}/api/mcp/send-message`, {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: {
|
|
63
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
64
|
+
"Content-Type": "application/json",
|
|
65
|
+
},
|
|
66
|
+
body: JSON.stringify(params),
|
|
67
|
+
});
|
|
68
|
+
if (res.status === 401) {
|
|
69
|
+
throw new Error("Invalid API key. Check your ONEKEY_API_KEY environment variable. " +
|
|
70
|
+
"Get a key at https://1key4ai.com/claude-code/setup");
|
|
71
|
+
}
|
|
72
|
+
if (res.status === 402) {
|
|
73
|
+
const data = await res.json().catch(() => ({}));
|
|
74
|
+
throw new Error(`Insufficient credits: ${String(data.message || "No balance remaining")}. ` +
|
|
75
|
+
`Add credits: ${String(data.add_credits_url || "https://1key4ai.com/billing")}`);
|
|
76
|
+
}
|
|
77
|
+
if (res.status === 409) {
|
|
78
|
+
throw new Error("Agent is already working on this session. Wait for it to finish or check status.");
|
|
79
|
+
}
|
|
80
|
+
if (res.status === 400) {
|
|
81
|
+
const data = await res.json().catch(() => ({}));
|
|
82
|
+
throw new Error(`Bad request: ${String(data.error || "Invalid input")}`);
|
|
83
|
+
}
|
|
84
|
+
if (!res.ok) {
|
|
85
|
+
const err = await res.text();
|
|
86
|
+
throw new Error(`Send message failed (${res.status}): ${err}`);
|
|
87
|
+
}
|
|
88
|
+
return await res.json();
|
|
89
|
+
}
|
|
90
|
+
async function getSessionStatus(sessionId) {
|
|
91
|
+
const res = await fetchWithRetry(`${API_BASE}/api/mcp/session/${encodeURIComponent(sessionId)}/status`, { headers: { Authorization: `Bearer ${API_KEY}` } });
|
|
92
|
+
if (res.status === 401) {
|
|
93
|
+
throw new Error("Invalid API key. Check your ONEKEY_API_KEY environment variable.");
|
|
94
|
+
}
|
|
95
|
+
if (!res.ok) {
|
|
96
|
+
const err = await res.text();
|
|
97
|
+
throw new Error(`Get session status failed (${res.status}): ${err}`);
|
|
98
|
+
}
|
|
99
|
+
return await res.json();
|
|
100
|
+
}
|
|
101
|
+
async function listSessionFiles(sessionId) {
|
|
102
|
+
const res = await fetchWithRetry(`${API_BASE}/api/mcp/session/${encodeURIComponent(sessionId)}/files`, { headers: { Authorization: `Bearer ${API_KEY}` } });
|
|
103
|
+
if (res.status === 401) {
|
|
104
|
+
throw new Error("Invalid API key. Check your ONEKEY_API_KEY environment variable.");
|
|
105
|
+
}
|
|
106
|
+
if (!res.ok) {
|
|
107
|
+
const err = await res.text();
|
|
108
|
+
throw new Error(`List files failed (${res.status}): ${err}`);
|
|
109
|
+
}
|
|
110
|
+
return await res.json();
|
|
111
|
+
}
|
|
112
|
+
async function getSessionFile(sessionId, filePath) {
|
|
113
|
+
const encodedPath = filePath.split("/").map(s => encodeURIComponent(s)).join("/");
|
|
114
|
+
const res = await fetchWithRetry(`${API_BASE}/api/mcp/session/${encodeURIComponent(sessionId)}/files/${encodedPath}`, { headers: { Authorization: `Bearer ${API_KEY}` } });
|
|
115
|
+
if (res.status === 401) {
|
|
116
|
+
throw new Error("Invalid API key. Check your ONEKEY_API_KEY environment variable.");
|
|
117
|
+
}
|
|
118
|
+
if (res.status === 404) {
|
|
119
|
+
throw new Error(`File not found: ${filePath}`);
|
|
120
|
+
}
|
|
121
|
+
if (!res.ok) {
|
|
122
|
+
const err = await res.text();
|
|
123
|
+
throw new Error(`Get file failed (${res.status}): ${err}`);
|
|
124
|
+
}
|
|
125
|
+
return await res.json();
|
|
126
|
+
}
|
|
127
|
+
async function answerQuestionnaire(sessionId, answers) {
|
|
128
|
+
const res = await fetchWithRetry(`${API_BASE}/api/mcp/session/${encodeURIComponent(sessionId)}/questionnaire`, {
|
|
129
|
+
method: "POST",
|
|
130
|
+
headers: {
|
|
131
|
+
Authorization: `Bearer ${API_KEY}`,
|
|
132
|
+
"Content-Type": "application/json",
|
|
133
|
+
},
|
|
134
|
+
body: JSON.stringify({ answers }),
|
|
135
|
+
});
|
|
136
|
+
if (res.status === 401) {
|
|
137
|
+
throw new Error("Invalid API key. Check your ONEKEY_API_KEY environment variable.");
|
|
138
|
+
}
|
|
139
|
+
if (res.status === 402) {
|
|
140
|
+
const data = await res.json().catch(() => ({}));
|
|
141
|
+
throw new Error(`Insufficient credits: ${String(data.message || "No balance remaining")}. ` +
|
|
142
|
+
`Add credits: ${String(data.add_credits_url || "https://1key4ai.com/billing")}`);
|
|
143
|
+
}
|
|
144
|
+
if (res.status === 400) {
|
|
145
|
+
const data = await res.json().catch(() => ({}));
|
|
146
|
+
throw new Error(`Bad request: ${String(data.error || "No pending questionnaire")}`);
|
|
147
|
+
}
|
|
148
|
+
if (!res.ok) {
|
|
149
|
+
const err = await res.text();
|
|
150
|
+
throw new Error(`Answer questionnaire failed (${res.status}): ${err}`);
|
|
151
|
+
}
|
|
152
|
+
return await res.json();
|
|
153
|
+
}
|
|
154
|
+
async function pollUntilDone(sessionId) {
|
|
155
|
+
const pollInterval = 5000;
|
|
156
|
+
const maxWait = 600000;
|
|
157
|
+
const start = Date.now();
|
|
158
|
+
while (Date.now() - start < maxWait) {
|
|
159
|
+
await new Promise(r => setTimeout(r, pollInterval));
|
|
160
|
+
let pollRes;
|
|
161
|
+
try {
|
|
162
|
+
pollRes = await fetchWithRetry(`${API_BASE}/api/mcp/session/${encodeURIComponent(sessionId)}/status`, { headers: { Authorization: `Bearer ${API_KEY}` } });
|
|
163
|
+
}
|
|
164
|
+
catch (networkErr) {
|
|
165
|
+
console.error(`Poll network error (will retry): ${networkErr}`);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (pollRes.status === 401) {
|
|
169
|
+
throw new Error("Invalid API key during poll.");
|
|
170
|
+
}
|
|
171
|
+
if (!pollRes.ok) {
|
|
172
|
+
throw new Error(`Poll failed (${pollRes.status})`);
|
|
173
|
+
}
|
|
174
|
+
const data = await pollRes.json();
|
|
175
|
+
if (data.status === "working") {
|
|
176
|
+
console.error(`Session ${sessionId} still working...`);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
return data;
|
|
180
|
+
}
|
|
181
|
+
throw new Error("Agent timed out after 10 minutes. Use 1k4_get_session_status to check later.");
|
|
182
|
+
}
|
|
183
|
+
function formatBridgeResponse(data, sessionId) {
|
|
184
|
+
const lines = [];
|
|
185
|
+
if (data.response) {
|
|
186
|
+
lines.push(String(data.response));
|
|
187
|
+
lines.push("");
|
|
188
|
+
}
|
|
189
|
+
if (data.status === "waiting_for_input" && data.pending_questionnaire) {
|
|
190
|
+
const questionnaire = data.pending_questionnaire;
|
|
191
|
+
if (questionnaire.intro) {
|
|
192
|
+
lines.push(String(questionnaire.intro));
|
|
193
|
+
lines.push("");
|
|
194
|
+
}
|
|
195
|
+
lines.push("The agent needs your answers:");
|
|
196
|
+
const items = (questionnaire.items || questionnaire.questions);
|
|
197
|
+
if (items && Array.isArray(items)) {
|
|
198
|
+
for (let i = 0; i < items.length; i++) {
|
|
199
|
+
const q = items[i];
|
|
200
|
+
const qType = q.type ? ` [${q.type}]` : "";
|
|
201
|
+
lines.push(` ${i + 1}. ${String(q.question || q.text || q.label || "")}${qType}`);
|
|
202
|
+
if (q.options && Array.isArray(q.options)) {
|
|
203
|
+
for (const opt of q.options) {
|
|
204
|
+
const label = typeof opt === "string" ? opt : String(opt.label || opt);
|
|
205
|
+
const rec = (typeof opt === "object" && opt.recommended) ? " (recommended)" : "";
|
|
206
|
+
lines.push(` - ${label}${rec}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (q.recommendation) {
|
|
210
|
+
lines.push(` Suggested: ${q.recommendation}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
lines.push("");
|
|
215
|
+
lines.push("Use 1k4_answer_questionnaire with the session_id and your answers.");
|
|
216
|
+
lines.push("");
|
|
217
|
+
}
|
|
218
|
+
if (data.status === "error") {
|
|
219
|
+
lines.push("Status: error");
|
|
220
|
+
lines.push("");
|
|
221
|
+
}
|
|
222
|
+
const filesCreated = data.files_created;
|
|
223
|
+
if (filesCreated && filesCreated.length > 0) {
|
|
224
|
+
lines.push(`Files created/modified (${filesCreated.length}):`);
|
|
225
|
+
for (const f of filesCreated) {
|
|
226
|
+
lines.push(` - ${f}`);
|
|
227
|
+
}
|
|
228
|
+
lines.push("");
|
|
229
|
+
}
|
|
230
|
+
if (data.files_count) {
|
|
231
|
+
lines.push(`Total files in project: ${data.files_count}`);
|
|
232
|
+
}
|
|
233
|
+
lines.push(`session_id: ${sessionId}`);
|
|
234
|
+
if (data.studio_url) {
|
|
235
|
+
lines.push(`Studio URL: ${data.studio_url}`);
|
|
236
|
+
}
|
|
237
|
+
return lines.join("\n");
|
|
238
|
+
}
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
// MCP Server
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
const server = new Server({ name: "1k4-studio", version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
243
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
244
|
+
tools: [
|
|
245
|
+
{
|
|
246
|
+
name: "1k4_send_message",
|
|
247
|
+
description: "Send a message to a 1K4 Studio agent session. The agent processes your " +
|
|
248
|
+
"request (code generation, analysis, file creation, etc.) and this tool " +
|
|
249
|
+
"polls until the agent finishes. Returns session_id. Pass this session_id " +
|
|
250
|
+
"to all subsequent calls to continue the conversation in the same session. " +
|
|
251
|
+
"When acting as the session's external agent (after 1k4_wait_for_message), " +
|
|
252
|
+
"pass role='assistant' to send your response without triggering the Studio agent.",
|
|
253
|
+
inputSchema: {
|
|
254
|
+
type: "object",
|
|
255
|
+
properties: {
|
|
256
|
+
message: {
|
|
257
|
+
type: "string",
|
|
258
|
+
description: "The message to send to the agent.",
|
|
259
|
+
},
|
|
260
|
+
session_id: {
|
|
261
|
+
type: "string",
|
|
262
|
+
description: "Session ID from a previous call. Omit on first call to auto-create a new session.",
|
|
263
|
+
},
|
|
264
|
+
model: {
|
|
265
|
+
type: "string",
|
|
266
|
+
description: "Optional model override (e.g., 'claude-sonnet-4-20250514').",
|
|
267
|
+
},
|
|
268
|
+
role: {
|
|
269
|
+
type: "string",
|
|
270
|
+
description: "Message role: 'user' (default, triggers Studio agent) or 'assistant' " +
|
|
271
|
+
"(external-agent response, no agent task). Use 'assistant' only after " +
|
|
272
|
+
"1k4_wait_for_message has set external_agent mode on the session.",
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
required: ["message"],
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: "1k4_wait_for_message",
|
|
280
|
+
description: "Wait for a user message in a 1K4 Studio session. Long-polls the " +
|
|
281
|
+
"backend (up to 5 minutes internally) and returns when the user types " +
|
|
282
|
+
"a message in the Studio UI. Use in a loop to act as the session's " +
|
|
283
|
+
"agent: wait for message -> process -> 1k4_send_message(role='assistant') -> repeat. " +
|
|
284
|
+
"On first call (no after_seq), initializes a cursor and starts listening. " +
|
|
285
|
+
"Pass the returned 'seq' as 'after_seq' on subsequent calls. " +
|
|
286
|
+
"Sets the session to external_agent mode (suppresses Studio agent tasks). " +
|
|
287
|
+
"Auto-reverts after 2 min of no polling if Claude Code disconnects.",
|
|
288
|
+
inputSchema: {
|
|
289
|
+
type: "object",
|
|
290
|
+
properties: {
|
|
291
|
+
session_id: {
|
|
292
|
+
type: "string",
|
|
293
|
+
description: "The session to listen on.",
|
|
294
|
+
},
|
|
295
|
+
after_seq: {
|
|
296
|
+
type: "string",
|
|
297
|
+
description: "Sequence cursor from previous response. Omit on first call.",
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
required: ["session_id"],
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
name: "1k4_get_session_status",
|
|
305
|
+
description: "Check the current status of a 1K4 Studio session. Returns instantly " +
|
|
306
|
+
"(no polling). Use to check if an agent is still working, or to see the " +
|
|
307
|
+
"latest response and files after a timeout.",
|
|
308
|
+
inputSchema: {
|
|
309
|
+
type: "object",
|
|
310
|
+
properties: {
|
|
311
|
+
session_id: {
|
|
312
|
+
type: "string",
|
|
313
|
+
description: "The session ID to check.",
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
required: ["session_id"],
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
name: "1k4_list_files",
|
|
321
|
+
description: "List all files in a 1K4 Studio project workspace. Returns file names, " +
|
|
322
|
+
"paths, mime types, and sizes.",
|
|
323
|
+
inputSchema: {
|
|
324
|
+
type: "object",
|
|
325
|
+
properties: {
|
|
326
|
+
session_id: {
|
|
327
|
+
type: "string",
|
|
328
|
+
description: "The session ID (used to resolve the project workspace).",
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
required: ["session_id"],
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: "1k4_get_file",
|
|
336
|
+
description: "Download the content of a specific file from a 1K4 Studio project workspace. " +
|
|
337
|
+
"For text files, returns the full content. For binary files (images, etc.), " +
|
|
338
|
+
"returns metadata only.",
|
|
339
|
+
inputSchema: {
|
|
340
|
+
type: "object",
|
|
341
|
+
properties: {
|
|
342
|
+
session_id: {
|
|
343
|
+
type: "string",
|
|
344
|
+
description: "The session ID (used to resolve the project workspace).",
|
|
345
|
+
},
|
|
346
|
+
file_path: {
|
|
347
|
+
type: "string",
|
|
348
|
+
description: "Path to the file (e.g., 'src/index.ts'). Use paths from 1k4_list_files.",
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
required: ["session_id", "file_path"],
|
|
352
|
+
},
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
name: "1k4_answer_questionnaire",
|
|
356
|
+
description: "Answer a pending questionnaire from the 1K4 Studio agent. When a session " +
|
|
357
|
+
"has status 'waiting_for_input', the agent needs answers before proceeding. " +
|
|
358
|
+
"Polls until the agent finishes processing the answers.",
|
|
359
|
+
inputSchema: {
|
|
360
|
+
type: "object",
|
|
361
|
+
properties: {
|
|
362
|
+
session_id: {
|
|
363
|
+
type: "string",
|
|
364
|
+
description: "The session ID with the pending questionnaire.",
|
|
365
|
+
},
|
|
366
|
+
answers: {
|
|
367
|
+
type: "array",
|
|
368
|
+
items: {
|
|
369
|
+
type: "object",
|
|
370
|
+
properties: {
|
|
371
|
+
question_index: {
|
|
372
|
+
type: "number",
|
|
373
|
+
description: "Zero-based index of the question being answered.",
|
|
374
|
+
},
|
|
375
|
+
value: {
|
|
376
|
+
description: "The answer. String for single-choice, array of strings for multi-select.",
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
required: ["question_index", "value"],
|
|
380
|
+
},
|
|
381
|
+
description: "Array of answers, one per question.",
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
required: ["session_id", "answers"],
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
],
|
|
388
|
+
}));
|
|
389
|
+
// Handle tool calls
|
|
390
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
391
|
+
const toolName = request.params.name;
|
|
392
|
+
// -----------------------------------------------------------------------
|
|
393
|
+
// 1k4_send_message
|
|
394
|
+
// -----------------------------------------------------------------------
|
|
395
|
+
if (toolName === "1k4_send_message") {
|
|
396
|
+
const args = request.params.arguments;
|
|
397
|
+
if (!args.message) {
|
|
398
|
+
return {
|
|
399
|
+
content: [{ type: "text", text: "'message' is required." }],
|
|
400
|
+
isError: true,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
const sendBody = {
|
|
405
|
+
message: args.message,
|
|
406
|
+
session_id: args.session_id,
|
|
407
|
+
model: args.model,
|
|
408
|
+
role: args.role,
|
|
409
|
+
};
|
|
410
|
+
const result = await sendMessage(sendBody);
|
|
411
|
+
const sessionId = String(result.session_id);
|
|
412
|
+
// role=assistant responses are complete immediately (no agent task)
|
|
413
|
+
if (args.role === "assistant") {
|
|
414
|
+
return {
|
|
415
|
+
content: [{ type: "text", text: `Response sent.\n\nsession_id: ${sessionId}` }],
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
console.error(`Message sent, session=${sessionId}, polling for completion...`);
|
|
419
|
+
const data = await pollUntilDone(sessionId);
|
|
420
|
+
return {
|
|
421
|
+
content: [{ type: "text", text: formatBridgeResponse(data, sessionId) }],
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
catch (e) {
|
|
425
|
+
return {
|
|
426
|
+
content: [{ type: "text", text: `Send message error: ${e}` }],
|
|
427
|
+
isError: true,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// -----------------------------------------------------------------------
|
|
432
|
+
// 1k4_wait_for_message
|
|
433
|
+
// -----------------------------------------------------------------------
|
|
434
|
+
if (toolName === "1k4_wait_for_message") {
|
|
435
|
+
const args = request.params.arguments;
|
|
436
|
+
if (!args.session_id) {
|
|
437
|
+
return {
|
|
438
|
+
content: [{ type: "text", text: "'session_id' is required." }],
|
|
439
|
+
isError: true,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
try {
|
|
443
|
+
let seq = args.after_seq || "";
|
|
444
|
+
const maxIterations = 10;
|
|
445
|
+
for (let i = 0; i < maxIterations; i++) {
|
|
446
|
+
const params = new URLSearchParams({ timeout: "30" });
|
|
447
|
+
if (seq)
|
|
448
|
+
params.set("after_seq", seq);
|
|
449
|
+
console.error(`[1k4] Waiting for message (${i + 1}/${maxIterations})...`);
|
|
450
|
+
const controller = new AbortController();
|
|
451
|
+
const timeoutId = setTimeout(() => controller.abort(), 35_000);
|
|
452
|
+
let res;
|
|
453
|
+
try {
|
|
454
|
+
res = await fetch(`${API_BASE}/api/mcp/session/${encodeURIComponent(args.session_id)}/wait?${params}`, {
|
|
455
|
+
headers: { Authorization: `Bearer ${API_KEY}` },
|
|
456
|
+
signal: controller.signal,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
finally {
|
|
460
|
+
clearTimeout(timeoutId);
|
|
461
|
+
}
|
|
462
|
+
if (!res.ok) {
|
|
463
|
+
const err = await res.text();
|
|
464
|
+
const seqSuffix = seq ? `\nafter_seq: ${seq}` : "";
|
|
465
|
+
return {
|
|
466
|
+
content: [{ type: "text", text: `Wait error (${res.status}): ${err}${seqSuffix}\nsession_id: ${args.session_id}` }],
|
|
467
|
+
isError: true,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
const data = await res.json();
|
|
471
|
+
if (data.has_message) {
|
|
472
|
+
const msg = data.message;
|
|
473
|
+
const lines = [
|
|
474
|
+
"User message received:",
|
|
475
|
+
"",
|
|
476
|
+
String(msg.content || ""),
|
|
477
|
+
"",
|
|
478
|
+
`type: ${msg.message_type || "text"}`,
|
|
479
|
+
`seq: ${data.seq}`,
|
|
480
|
+
`session_id: ${data.session_id}`,
|
|
481
|
+
];
|
|
482
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
483
|
+
}
|
|
484
|
+
seq = String(data.seq || seq);
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
content: [{ type: "text", text: `No new message after ${maxIterations * 30}s.\n\nseq: ${seq}\nsession_id: ${args.session_id}` }],
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
catch (e) {
|
|
491
|
+
const seqSuffix = args.after_seq ? `\nafter_seq: ${args.after_seq}` : "";
|
|
492
|
+
if (e.name === "AbortError") {
|
|
493
|
+
return { content: [{ type: "text", text: `Wait timed out (network).${seqSuffix}\nsession_id: ${args.session_id}` }] };
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
content: [{ type: "text", text: `Wait error: ${e}${seqSuffix}\nsession_id: ${args.session_id}` }],
|
|
497
|
+
isError: true,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
// -----------------------------------------------------------------------
|
|
502
|
+
// 1k4_get_session_status
|
|
503
|
+
// -----------------------------------------------------------------------
|
|
504
|
+
if (toolName === "1k4_get_session_status") {
|
|
505
|
+
const args = request.params.arguments;
|
|
506
|
+
if (!args.session_id) {
|
|
507
|
+
return {
|
|
508
|
+
content: [{ type: "text", text: "'session_id' is required." }],
|
|
509
|
+
isError: true,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
const data = await getSessionStatus(args.session_id);
|
|
514
|
+
return {
|
|
515
|
+
content: [{ type: "text", text: formatBridgeResponse(data, args.session_id) }],
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
catch (e) {
|
|
519
|
+
return {
|
|
520
|
+
content: [{ type: "text", text: `Get session status error: ${e}` }],
|
|
521
|
+
isError: true,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
// -----------------------------------------------------------------------
|
|
526
|
+
// 1k4_list_files
|
|
527
|
+
// -----------------------------------------------------------------------
|
|
528
|
+
if (toolName === "1k4_list_files") {
|
|
529
|
+
const args = request.params.arguments;
|
|
530
|
+
if (!args.session_id) {
|
|
531
|
+
return {
|
|
532
|
+
content: [{ type: "text", text: "'session_id' is required." }],
|
|
533
|
+
isError: true,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
try {
|
|
537
|
+
const data = await listSessionFiles(args.session_id);
|
|
538
|
+
const files = data.files;
|
|
539
|
+
if (!files || files.length === 0) {
|
|
540
|
+
return {
|
|
541
|
+
content: [{ type: "text", text: `No files in project workspace.\n\nsession_id: ${args.session_id}` }],
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
const lines = [`Files (${files.length}):`];
|
|
545
|
+
for (const f of files) {
|
|
546
|
+
const size = f.size ? ` (${f.size} bytes)` : "";
|
|
547
|
+
lines.push(` ${f.path || f.name}${size}`);
|
|
548
|
+
}
|
|
549
|
+
lines.push("");
|
|
550
|
+
lines.push(`session_id: ${args.session_id}`);
|
|
551
|
+
return {
|
|
552
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
catch (e) {
|
|
556
|
+
return {
|
|
557
|
+
content: [{ type: "text", text: `List files error: ${e}` }],
|
|
558
|
+
isError: true,
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
// -----------------------------------------------------------------------
|
|
563
|
+
// 1k4_get_file
|
|
564
|
+
// -----------------------------------------------------------------------
|
|
565
|
+
if (toolName === "1k4_get_file") {
|
|
566
|
+
const args = request.params.arguments;
|
|
567
|
+
if (!args.session_id || !args.file_path) {
|
|
568
|
+
return {
|
|
569
|
+
content: [{ type: "text", text: "'session_id' and 'file_path' are required." }],
|
|
570
|
+
isError: true,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
try {
|
|
574
|
+
const data = await getSessionFile(args.session_id, args.file_path);
|
|
575
|
+
if (data.binary) {
|
|
576
|
+
return {
|
|
577
|
+
content: [{
|
|
578
|
+
type: "text",
|
|
579
|
+
text: `Binary file: ${args.file_path} (${data.mime_type}, ${data.size_bytes} bytes)\n` +
|
|
580
|
+
`${data.message || "Use Studio URL to view."}\n\nsession_id: ${args.session_id}`,
|
|
581
|
+
}],
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
return {
|
|
585
|
+
content: [{
|
|
586
|
+
type: "text",
|
|
587
|
+
text: `File: ${args.file_path}\n\n${String(data.content || "")}\n\nsession_id: ${args.session_id}`,
|
|
588
|
+
}],
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
catch (e) {
|
|
592
|
+
return {
|
|
593
|
+
content: [{ type: "text", text: `Get file error: ${e}` }],
|
|
594
|
+
isError: true,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
// -----------------------------------------------------------------------
|
|
599
|
+
// 1k4_answer_questionnaire
|
|
600
|
+
// -----------------------------------------------------------------------
|
|
601
|
+
if (toolName === "1k4_answer_questionnaire") {
|
|
602
|
+
const args = request.params.arguments;
|
|
603
|
+
if (!args.session_id || !args.answers) {
|
|
604
|
+
return {
|
|
605
|
+
content: [{ type: "text", text: "'session_id' and 'answers' are required." }],
|
|
606
|
+
isError: true,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
try {
|
|
610
|
+
await answerQuestionnaire(args.session_id, args.answers);
|
|
611
|
+
console.error(`Questionnaire answered, session=${args.session_id}, polling for completion...`);
|
|
612
|
+
const data = await pollUntilDone(args.session_id);
|
|
613
|
+
return {
|
|
614
|
+
content: [{ type: "text", text: formatBridgeResponse(data, args.session_id) }],
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
catch (e) {
|
|
618
|
+
return {
|
|
619
|
+
content: [{ type: "text", text: `Answer questionnaire error: ${e}` }],
|
|
620
|
+
isError: true,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return {
|
|
625
|
+
content: [{ type: "text", text: `Unknown tool: ${toolName}` }],
|
|
626
|
+
isError: true,
|
|
627
|
+
};
|
|
628
|
+
});
|
|
629
|
+
// Start
|
|
630
|
+
async function main() {
|
|
631
|
+
const transport = new StdioServerTransport();
|
|
632
|
+
await server.connect(transport);
|
|
633
|
+
console.error("1K4 Studio MCP server running");
|
|
634
|
+
}
|
|
635
|
+
main().catch((e) => {
|
|
636
|
+
console.error("Fatal:", e);
|
|
637
|
+
process.exit(1);
|
|
638
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@1key4ai/mcp-studio",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "1K4 Studio bridge for Claude Code. Send messages, manage files, and act as a Studio agent via MCP tools.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"claude-code",
|
|
7
|
+
"mcp",
|
|
8
|
+
"studio",
|
|
9
|
+
"ai",
|
|
10
|
+
"onekey",
|
|
11
|
+
"1k4"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "OneKey <hello@1key4ai.com>",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/OneKey-Incorporated/onekey",
|
|
18
|
+
"directory": "packages/mcp-studio"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://1key4ai.com/claude-code",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"main": "dist/index.js",
|
|
23
|
+
"bin": {
|
|
24
|
+
"mcp-studio": "dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc",
|
|
31
|
+
"dev": "tsx src/index.ts",
|
|
32
|
+
"prepublishOnly": "npm run build"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/node": "^20.0.0",
|
|
39
|
+
"tsx": "^4.7.0",
|
|
40
|
+
"typescript": "^5.4.0"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18"
|
|
44
|
+
}
|
|
45
|
+
}
|