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 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.3.tgz`.
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",
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",
@@ -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',
@@ -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
- sendJson(res, 410, { error: 'Legacy SSE transport removed. Use POST /mcp.' });
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
- sendJson(res, 410, { error: 'Legacy SSE transport removed. Use POST /mcp.' });
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
+ });