kiku-agent 1.3.3 ā 1.3.5
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 +1 -1
- package/package.json +2 -1
- package/server/apiRouter.js +11 -0
- package/server/mcp/handler.js +42 -2
- package/server/mcp/sseServer.js +146 -0
- package/server/mcp/tests/connection.test.js +41 -0
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ kiku-agent
|
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
> [!NOTE]
|
|
55
|
-
> The global package deliberately excludes native binary build dependencies (Python, `node-gyp`, Visual Studio C++ build tools are not required). For a local checkout before publishing, pack and install via `npm install -g ./kiku-agent-1.3.
|
|
55
|
+
> The global package deliberately excludes native binary build dependencies (Python, `node-gyp`, Visual Studio C++ build tools are not required). For a local checkout before publishing, pack and install via `npm install -g ./kiku-agent-1.3.5.tgz`.
|
|
56
56
|
|
|
57
57
|
### Zero-Install One-Shot Query
|
|
58
58
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kiku-agent",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.5",
|
|
4
4
|
"description": "Kiku AI terminal agent and MCP server for Ju Jingyi knowledge, media, and career data.",
|
|
5
5
|
"license": "GPL-3.0",
|
|
6
6
|
"type": "module",
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
"kiku-agent": "node server/agent/cli.js",
|
|
60
60
|
"web-chat": "node server/web-chat/server.js --port 3002 --open",
|
|
61
61
|
"kiku-mcp": "node server/mcp/stdio.js",
|
|
62
|
+
"mcp:sse": "node server/mcp/sseServer.js",
|
|
62
63
|
"kiku-ink": "npx tsx server/agent/ink/main.jsx",
|
|
63
64
|
"vram:live": "node server/agent/cli.js --vram",
|
|
64
65
|
"profile:nsys": "node scripts/profileNsys.js",
|
package/server/apiRouter.js
CHANGED
|
@@ -5500,6 +5500,12 @@ function getComprehensiveGalleryList() {
|
|
|
5500
5500
|
|
|
5501
5501
|
if (pathname === '/mcp') {
|
|
5502
5502
|
if (req.method === 'GET') {
|
|
5503
|
+
const accept = req.headers.accept || '';
|
|
5504
|
+
if (accept.includes('text/event-stream') || req.url.includes('format=sse')) {
|
|
5505
|
+
const { handleMcpSse } = await import('./mcp/handler.js');
|
|
5506
|
+
return await handleMcpSse(req, res);
|
|
5507
|
+
}
|
|
5508
|
+
|
|
5503
5509
|
const { getMcpSessionStats } = await import('./mcp/handler.js');
|
|
5504
5510
|
const { toolDefinitions } = await import('./mcp/tools.js');
|
|
5505
5511
|
const { resourceDefinitions } = await import('./mcp/resources.js');
|
|
@@ -5539,6 +5545,11 @@ function getComprehensiveGalleryList() {
|
|
|
5539
5545
|
}
|
|
5540
5546
|
}
|
|
5541
5547
|
|
|
5548
|
+
if (pathname === '/mcp/messages' && req.method === 'POST') {
|
|
5549
|
+
const { handleMcpMessage } = await import('./mcp/handler.js');
|
|
5550
|
+
return await handleMcpMessage(req, res);
|
|
5551
|
+
}
|
|
5552
|
+
|
|
5542
5553
|
if (pathname === '/mcp/sse' || pathname === '/mcp/message') {
|
|
5543
5554
|
return sendJson(res, 410, {
|
|
5544
5555
|
status: 'error',
|
package/server/mcp/handler.js
CHANGED
|
@@ -19,12 +19,52 @@ function validateRequestSize(req, res) {
|
|
|
19
19
|
return true;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
23
|
+
|
|
24
|
+
const sseSessions = new Map();
|
|
25
|
+
|
|
22
26
|
export async function handleMcpSse(req, res) {
|
|
23
|
-
|
|
27
|
+
const security = await securityMiddleware(req, res);
|
|
28
|
+
if (security.blocked) return;
|
|
29
|
+
|
|
30
|
+
const auth = security.auth || await validateApiKey(req);
|
|
31
|
+
const rateBucket = auth.keyPrefix ? `mcp-key:${auth.keyPrefix}` : `mcp-sse:${req.socket?.remoteAddress || 'unknown'}`;
|
|
32
|
+
const maxLimit = auth.rateLimit || 30;
|
|
33
|
+
const rate = checkRateLimit(rateBucket, maxLimit);
|
|
34
|
+
if (!rate.allowed) return sendJson(res, 429, { error: rate.error }, { 'Retry-After': '60' });
|
|
35
|
+
|
|
36
|
+
const authorized = auth.valid && auth.authorized !== false;
|
|
37
|
+
const server = createMcpServer({ transport: 'sse', authorized, tier: auth.tier });
|
|
38
|
+
const transport = new SSEServerTransport('/mcp/messages', res);
|
|
39
|
+
sseSessions.set(transport.sessionId, { transport, server });
|
|
40
|
+
|
|
41
|
+
res.on('close', () => {
|
|
42
|
+
sseSessions.delete(transport.sessionId);
|
|
43
|
+
server.close().catch(() => {});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
await server.connect(transport);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
console.error(`MCP SSE error: ${err.message}`);
|
|
50
|
+
sseSessions.delete(transport.sessionId);
|
|
51
|
+
}
|
|
24
52
|
}
|
|
25
53
|
|
|
26
54
|
export async function handleMcpMessage(req, res) {
|
|
27
|
-
|
|
55
|
+
const url = new URL(req.url, 'http://localhost');
|
|
56
|
+
const sessionId = url.searchParams.get('sessionId');
|
|
57
|
+
const session = sessionId ? sseSessions.get(sessionId) : null;
|
|
58
|
+
if (!session) {
|
|
59
|
+
return sendJson(res, 404, { error: 'SSE session not found or expired. Re-connect to /mcp.' });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
await session.transport.handlePostMessage(req, res);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error(`MCP SSE message error: ${err.message}`);
|
|
66
|
+
if (!res.headersSent) sendJson(res, 500, { error: err.message });
|
|
67
|
+
}
|
|
28
68
|
}
|
|
29
69
|
|
|
30
70
|
export async function handleMcpHttp(req, res) {
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ju Jingyi MCP Remote SSE Server (OpenCode & Open WebUI Compatible)
|
|
5
|
+
*
|
|
6
|
+
* Provides an SSE (Server-Sent Events) Model Context Protocol endpoint
|
|
7
|
+
* supporting remote AI clients like OpenCode, Open WebUI, and legacy MCP consumers.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import http from 'node:http';
|
|
11
|
+
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
12
|
+
import { createMcpServer } from './server.js';
|
|
13
|
+
import { PACKAGE_VERSION } from '../agent/version.js';
|
|
14
|
+
|
|
15
|
+
const PORT = Number.parseInt(process.env.KIKU_MCP_PORT || process.env.PORT || '3005', 10);
|
|
16
|
+
const HOST = process.env.KIKU_MCP_HOST || '127.0.0.1';
|
|
17
|
+
|
|
18
|
+
const sessions = new Map();
|
|
19
|
+
|
|
20
|
+
function sendJson(res, statusCode, data, extraHeaders = {}) {
|
|
21
|
+
if (res.headersSent) return;
|
|
22
|
+
res.statusCode = statusCode;
|
|
23
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
24
|
+
for (const [k, v] of Object.entries(extraHeaders)) {
|
|
25
|
+
res.setHeader(k, v);
|
|
26
|
+
}
|
|
27
|
+
res.end(JSON.stringify(data, null, 2));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function createMcpSseHttpServer(options = {}) {
|
|
31
|
+
const server = http.createServer(async (req, res) => {
|
|
32
|
+
|
|
33
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
34
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
35
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key, Accept');
|
|
36
|
+
|
|
37
|
+
if (req.method === 'OPTIONS') {
|
|
38
|
+
res.statusCode = 204;
|
|
39
|
+
return res.end();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const host = req.headers.host || `${HOST}:${PORT}`;
|
|
43
|
+
const url = new URL(req.url, `http://${host}`);
|
|
44
|
+
const pathname = url.pathname.replace(/\/+$/, '') || '/';
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
if (pathname === '/mcp') {
|
|
48
|
+
if (req.method === 'GET') {
|
|
49
|
+
const accept = req.headers.accept || '';
|
|
50
|
+
const wantsSse = accept.includes('text/event-stream') || url.searchParams.get('format') === 'sse';
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
const rawAuth = req.headers['x-api-key']
|
|
54
|
+
|| (req.headers.authorization ? req.headers.authorization.replace(/^Bearer\s+/i, '') : '')
|
|
55
|
+
|| url.searchParams.get('apiKey')
|
|
56
|
+
|| '';
|
|
57
|
+
const apiKey = String(rawAuth).trim();
|
|
58
|
+
const authorized = Boolean(apiKey.startsWith('kiku_') || apiKey.length >= 16);
|
|
59
|
+
|
|
60
|
+
if (wantsSse) {
|
|
61
|
+
const mcpServer = createMcpServer({
|
|
62
|
+
transport: 'sse',
|
|
63
|
+
authorized,
|
|
64
|
+
tier: apiKey ? 'mcp_vip' : 'standard',
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
const transport = new SSEServerTransport('/mcp/messages', res);
|
|
69
|
+
sessions.set(transport.sessionId, { transport, mcpServer, createdAt: Date.now() });
|
|
70
|
+
|
|
71
|
+
res.on('close', () => {
|
|
72
|
+
sessions.delete(transport.sessionId);
|
|
73
|
+
mcpServer.close().catch(() => {});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
await mcpServer.connect(transport);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.error('[MCP SSE Server] Connect error:', err.message);
|
|
80
|
+
sessions.delete(transport.sessionId);
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
return sendJson(res, 200, {
|
|
87
|
+
status: 'success',
|
|
88
|
+
code: 200,
|
|
89
|
+
name: 'kiku',
|
|
90
|
+
title: 'kiku',
|
|
91
|
+
version: PACKAGE_VERSION,
|
|
92
|
+
protocol: 'mcp',
|
|
93
|
+
transport: 'sse',
|
|
94
|
+
endpoints: {
|
|
95
|
+
sse: `http://${host}/mcp`,
|
|
96
|
+
messages: `http://${host}/mcp/messages`
|
|
97
|
+
},
|
|
98
|
+
message: 'Ju Jingyi MCP SSE Server is active. Connect with OpenCode remote configuration.',
|
|
99
|
+
authReceived: Boolean(apiKey)
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if ((pathname === '/mcp/messages' || pathname === '/mcp') && req.method === 'POST') {
|
|
106
|
+
const sessionId = url.searchParams.get('sessionId');
|
|
107
|
+
const session = sessionId ? sessions.get(sessionId) : null;
|
|
108
|
+
|
|
109
|
+
if (!session) {
|
|
110
|
+
return sendJson(res, 404, {
|
|
111
|
+
status: 'error',
|
|
112
|
+
code: 404,
|
|
113
|
+
error: 'SSE session not found or expired. Please re-connect to /mcp.'
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
await session.transport.handlePostMessage(req, res);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
console.error('[MCP SSE Server] Message error:', err.message);
|
|
121
|
+
if (!res.headersSent) {
|
|
122
|
+
sendJson(res, 500, { error: err.message });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
sendJson(res, 404, {
|
|
130
|
+
status: 'error',
|
|
131
|
+
code: 404,
|
|
132
|
+
error: `Endpoint ${pathname} not found on MCP SSE Server.`
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return server;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (process.argv[1] && import.meta.url === `file:///${process.argv[1].replace(/\\/g, '/')}`) {
|
|
140
|
+
const server = createMcpSseHttpServer();
|
|
141
|
+
server.listen(PORT, HOST, () => {
|
|
142
|
+
console.log(`\nšø Ju Jingyi MCP Remote SSE Server v${PACKAGE_VERSION}`);
|
|
143
|
+
console.log(`š” URL: http://${HOST}:${PORT}/mcp`);
|
|
144
|
+
console.log(`ā” Ready for OpenCode remote connections on port ${PORT}\n`);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
@@ -160,3 +160,44 @@ test('Legacy MCP SSE and message routes respond with HTTP 410 Gone', async () =>
|
|
|
160
160
|
await new Promise(resolve => httpServer.close(resolve));
|
|
161
161
|
}
|
|
162
162
|
});
|
|
163
|
+
|
|
164
|
+
test('MCP client connects over SSE at /mcp with Accept: text/event-stream (OpenCode compatibility)', { timeout: 30_000 }, async () => {
|
|
165
|
+
const previousApiKey = process.env.MCP_API_KEY;
|
|
166
|
+
process.env.MCP_API_KEY = 'kiku_mcp_sk_0552855023fb5cb09707020b65000bd9';
|
|
167
|
+
const { SSEClientTransport } = await import('@modelcontextprotocol/sdk/client/sse.js');
|
|
168
|
+
const { handleApiRequest } = await import('../../apiRouter.js');
|
|
169
|
+
const httpServer = createServer((req, res) => {
|
|
170
|
+
void handleApiRequest(req, res).catch(error => {
|
|
171
|
+
res.statusCode = 500;
|
|
172
|
+
res.end(error.message);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
await new Promise((resolve, reject) => {
|
|
176
|
+
httpServer.once('error', reject);
|
|
177
|
+
httpServer.listen(0, '127.0.0.1', resolve);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const address = httpServer.address();
|
|
181
|
+
const baseUrl = `http://127.0.0.1:${address.port}`;
|
|
182
|
+
|
|
183
|
+
const transport = new SSEClientTransport(new URL(`${baseUrl}/mcp`), {
|
|
184
|
+
requestInit: { headers: { 'X-API-Key': 'kiku_mcp_sk_0552855023fb5cb09707020b65000bd9' } },
|
|
185
|
+
});
|
|
186
|
+
const client = new Client({ name: 'opencode-compat-test', version: PACKAGE_VERSION }, { capabilities: {} });
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
await client.connect(transport);
|
|
190
|
+
const tools = await client.listTools();
|
|
191
|
+
assert.ok(tools.tools.length >= 15);
|
|
192
|
+
assert.ok(tools.tools.some(t => t.name === 'search_works'));
|
|
193
|
+
const call = await client.callTool({ name: 'search_works', arguments: { query: 'In Blossom' } });
|
|
194
|
+
assert.equal(call.isError, false);
|
|
195
|
+
const payload = JSON.parse(call.content[0].text);
|
|
196
|
+
assert.equal(payload.status, 'success');
|
|
197
|
+
} finally {
|
|
198
|
+
await client.close().catch(() => {});
|
|
199
|
+
await new Promise(resolve => httpServer.close(resolve));
|
|
200
|
+
if (previousApiKey === undefined) delete process.env.MCP_API_KEY;
|
|
201
|
+
else process.env.MCP_API_KEY = previousApiKey;
|
|
202
|
+
}
|
|
203
|
+
});
|