kiku-agent 1.0.0 → 1.0.3
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 +293 -122
- package/package.json +60 -61
- package/server/agent/agentCore.js +892 -782
- package/server/agent/appPaths.js +21 -0
- package/server/agent/audioPlayer.js +739 -673
- package/server/agent/cli.js +1414 -1111
- package/server/agent/commandRegistry.js +74 -58
- package/server/agent/commands.js +1694 -788
- package/server/agent/config.js +264 -90
- package/server/agent/contextCompactor.js +79 -0
- package/server/agent/doctor.js +69 -0
- package/server/agent/eventBus.js +107 -103
- package/server/agent/gpuMetrics.js +189 -0
- package/server/agent/llmClient.js +369 -200
- package/server/agent/mcpClient.js +364 -0
- package/server/agent/memory.js +670 -566
- package/server/agent/modelManager.js +77 -0
- package/server/agent/research.js +202 -0
- package/server/agent/session.js +244 -239
- package/server/agent/subagent.js +116 -0
- package/server/agent/theme.js +125 -124
- package/server/agent/toolRouter.js +201 -0
- package/server/agent/tools.js +1679 -1528
- package/server/agent/ui.js +760 -469
- package/server/mcp/claude-desktop-config.example.json +1 -1
- package/server/mcp/data/discography.js +36 -36
- package/server/mcp/dataAdapter.js +102 -0
- package/server/mcp/examples/claude-code.mcp.example.json +19 -0
- package/server/mcp/examples/claude-desktop.example.json +11 -0
- package/server/mcp/examples/cline.example.json +13 -0
- package/server/mcp/examples/cursor.example.json +14 -0
- package/server/mcp/examples/gemini-cli.example.json +17 -0
- package/server/mcp/examples/opencode.example.json +21 -0
- package/server/mcp/examples/vscode.example.json +19 -0
- package/server/mcp/examples/windsurf.example.json +14 -0
- package/server/mcp/handler.js +85 -159
- package/server/mcp/security.js +140 -138
- package/server/mcp/server.js +129 -100
- package/server/mcp/stdio.js +4 -2
package/server/mcp/security.js
CHANGED
|
@@ -1,138 +1,140 @@
|
|
|
1
|
-
import { timingSafeEqual } from 'node:crypto';
|
|
2
|
-
|
|
3
|
-
const SECURITY_CONFIG = {
|
|
4
|
-
apiKeyRequired: !!process.env.MCP_API_KEY,
|
|
5
|
-
|
|
6
|
-
rateLimitWindowMs: 60_000,
|
|
7
|
-
rateLimitMaxRequests: 30,
|
|
8
|
-
|
|
9
|
-
maxConcurrentSessions: 100,
|
|
10
|
-
sessionTtlMs: 30 * 60_000,
|
|
11
|
-
maxMessagesPerSession: 500,
|
|
12
|
-
|
|
13
|
-
corsAllowedOrigins: (process.env.MCP_CORS_ORIGINS || '*').split(',').map(s => s.trim()),
|
|
14
|
-
maxRequestBodyBytes: 1_048_576,
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export const MAX_REQUEST_BODY_SIZE = SECURITY_CONFIG.maxRequestBodyBytes;
|
|
18
|
-
|
|
19
|
-
export function validateApiKey(req) {
|
|
20
|
-
if (!SECURITY_CONFIG.apiKeyRequired) return { valid: true };
|
|
21
|
-
|
|
22
|
-
let key = req.headers['x-api-key'];
|
|
23
|
-
if (!key) {
|
|
24
|
-
try {
|
|
25
|
-
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
26
|
-
key = url.searchParams.get('key') || url.searchParams.get('apiKey');
|
|
27
|
-
} catch {
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
if (!key) return { valid: false, error: 'Missing API key. Set X-API-Key header or ?key= query parameter.' };
|
|
31
|
-
const expected = Buffer.from(process.env.MCP_API_KEY || '');
|
|
32
|
-
const received = Buffer.from(String(key));
|
|
33
|
-
if (expected.length !== received.length || !timingSafeEqual(expected, received)) {
|
|
34
|
-
return { valid: false, error: 'Invalid API key.' };
|
|
35
|
-
}
|
|
36
|
-
return { valid: true };
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const rateLimitStore = new Map();
|
|
40
|
-
|
|
41
|
-
export function checkRateLimit(sessionId) {
|
|
42
|
-
const now = Date.now();
|
|
43
|
-
const entry = rateLimitStore.get(sessionId);
|
|
44
|
-
|
|
45
|
-
if (!entry || now - entry.windowStart > SECURITY_CONFIG.rateLimitWindowMs) {
|
|
46
|
-
rateLimitStore.set(sessionId, { count: 1, windowStart: now });
|
|
47
|
-
return { allowed: true, remaining: SECURITY_CONFIG.rateLimitMaxRequests - 1 };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
entry.count++;
|
|
51
|
-
const remaining = SECURITY_CONFIG.rateLimitMaxRequests - entry.count;
|
|
52
|
-
if (remaining < 0) {
|
|
53
|
-
return { allowed: false, error: `Rate limit exceeded. Max ${SECURITY_CONFIG.rateLimitMaxRequests} requests per minute.` };
|
|
54
|
-
}
|
|
55
|
-
return { allowed: true, remaining };
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const rateLimitCleanupTimer = setInterval(() => {
|
|
59
|
-
const now = Date.now();
|
|
60
|
-
for (const [id, entry] of rateLimitStore) {
|
|
61
|
-
if (now - entry.windowStart > SECURITY_CONFIG.rateLimitWindowMs * 2) {
|
|
62
|
-
rateLimitStore.delete(id);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}, 60_000);
|
|
66
|
-
rateLimitCleanupTimer.unref?.();
|
|
67
|
-
|
|
68
|
-
export function sanitizeInput(input) {
|
|
69
|
-
if (typeof input === 'string') {
|
|
70
|
-
return input
|
|
71
|
-
.replace(/[<>]/g, '')
|
|
72
|
-
.replace(/\x00/g, '')
|
|
73
|
-
.slice(0, 1000);
|
|
74
|
-
}
|
|
75
|
-
if (typeof input === 'object' && input !== null) {
|
|
76
|
-
const sanitized = {};
|
|
77
|
-
for (const [key, value] of Object.entries(input)) {
|
|
78
|
-
if (typeof key === 'string' && key.length <= 100) {
|
|
79
|
-
sanitized[key] = sanitizeInput(value);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
return sanitized;
|
|
83
|
-
}
|
|
84
|
-
return input;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function validateSessionId(sessionId) {
|
|
88
|
-
if (!sessionId || typeof sessionId !== 'string') return false;
|
|
89
|
-
return /^[a-zA-Z0-9_-]{1,100}$/.test(sessionId);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export function setCorsHeaders(res, origin) {
|
|
93
|
-
const allowed = SECURITY_CONFIG.corsAllowedOrigins;
|
|
94
|
-
const isAllowed = allowed.includes('*') || allowed.includes(origin);
|
|
95
|
-
|
|
96
|
-
if (isAllowed) {
|
|
97
|
-
res.setHeader('Access-Control-Allow-Origin', origin || '*');
|
|
98
|
-
} else {
|
|
99
|
-
res.setHeader('Access-Control-Allow-Origin', '');
|
|
100
|
-
}
|
|
101
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
102
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-API-Key');
|
|
103
|
-
res.setHeader('Access-Control-Max-Age', '86400');
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
res.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
res.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
1
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const SECURITY_CONFIG = {
|
|
4
|
+
apiKeyRequired: !!process.env.MCP_API_KEY,
|
|
5
|
+
|
|
6
|
+
rateLimitWindowMs: 60_000,
|
|
7
|
+
rateLimitMaxRequests: 30,
|
|
8
|
+
|
|
9
|
+
maxConcurrentSessions: 100,
|
|
10
|
+
sessionTtlMs: 30 * 60_000,
|
|
11
|
+
maxMessagesPerSession: 500,
|
|
12
|
+
|
|
13
|
+
corsAllowedOrigins: (process.env.MCP_CORS_ORIGINS || '*').split(',').map(s => s.trim()),
|
|
14
|
+
maxRequestBodyBytes: 1_048_576,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const MAX_REQUEST_BODY_SIZE = SECURITY_CONFIG.maxRequestBodyBytes;
|
|
18
|
+
|
|
19
|
+
export function validateApiKey(req) {
|
|
20
|
+
if (!SECURITY_CONFIG.apiKeyRequired) return { valid: true };
|
|
21
|
+
|
|
22
|
+
let key = req.headers['x-api-key'];
|
|
23
|
+
if (!key) {
|
|
24
|
+
try {
|
|
25
|
+
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
26
|
+
key = url.searchParams.get('key') || url.searchParams.get('apiKey');
|
|
27
|
+
} catch {
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (!key) return { valid: false, error: 'Missing API key. Set X-API-Key header or ?key= query parameter.' };
|
|
31
|
+
const expected = Buffer.from(process.env.MCP_API_KEY || '');
|
|
32
|
+
const received = Buffer.from(String(key));
|
|
33
|
+
if (expected.length !== received.length || !timingSafeEqual(expected, received)) {
|
|
34
|
+
return { valid: false, error: 'Invalid API key.' };
|
|
35
|
+
}
|
|
36
|
+
return { valid: true };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const rateLimitStore = new Map();
|
|
40
|
+
|
|
41
|
+
export function checkRateLimit(sessionId) {
|
|
42
|
+
const now = Date.now();
|
|
43
|
+
const entry = rateLimitStore.get(sessionId);
|
|
44
|
+
|
|
45
|
+
if (!entry || now - entry.windowStart > SECURITY_CONFIG.rateLimitWindowMs) {
|
|
46
|
+
rateLimitStore.set(sessionId, { count: 1, windowStart: now });
|
|
47
|
+
return { allowed: true, remaining: SECURITY_CONFIG.rateLimitMaxRequests - 1 };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
entry.count++;
|
|
51
|
+
const remaining = SECURITY_CONFIG.rateLimitMaxRequests - entry.count;
|
|
52
|
+
if (remaining < 0) {
|
|
53
|
+
return { allowed: false, error: `Rate limit exceeded. Max ${SECURITY_CONFIG.rateLimitMaxRequests} requests per minute.` };
|
|
54
|
+
}
|
|
55
|
+
return { allowed: true, remaining };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const rateLimitCleanupTimer = setInterval(() => {
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
for (const [id, entry] of rateLimitStore) {
|
|
61
|
+
if (now - entry.windowStart > SECURITY_CONFIG.rateLimitWindowMs * 2) {
|
|
62
|
+
rateLimitStore.delete(id);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}, 60_000);
|
|
66
|
+
rateLimitCleanupTimer.unref?.();
|
|
67
|
+
|
|
68
|
+
export function sanitizeInput(input) {
|
|
69
|
+
if (typeof input === 'string') {
|
|
70
|
+
return input
|
|
71
|
+
.replace(/[<>]/g, '')
|
|
72
|
+
.replace(/\x00/g, '')
|
|
73
|
+
.slice(0, 1000);
|
|
74
|
+
}
|
|
75
|
+
if (typeof input === 'object' && input !== null) {
|
|
76
|
+
const sanitized = {};
|
|
77
|
+
for (const [key, value] of Object.entries(input)) {
|
|
78
|
+
if (typeof key === 'string' && key.length <= 100) {
|
|
79
|
+
sanitized[key] = sanitizeInput(value);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return sanitized;
|
|
83
|
+
}
|
|
84
|
+
return input;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function validateSessionId(sessionId) {
|
|
88
|
+
if (!sessionId || typeof sessionId !== 'string') return false;
|
|
89
|
+
return /^[a-zA-Z0-9_-]{1,100}$/.test(sessionId);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function setCorsHeaders(res, origin) {
|
|
93
|
+
const allowed = SECURITY_CONFIG.corsAllowedOrigins;
|
|
94
|
+
const isAllowed = allowed.includes('*') || allowed.includes(origin);
|
|
95
|
+
|
|
96
|
+
if (isAllowed) {
|
|
97
|
+
res.setHeader('Access-Control-Allow-Origin', origin || '*');
|
|
98
|
+
} else {
|
|
99
|
+
res.setHeader('Access-Control-Allow-Origin', '');
|
|
100
|
+
}
|
|
101
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
102
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, X-API-Key, Accept');
|
|
103
|
+
res.setHeader('Access-Control-Max-Age', '86400');
|
|
104
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
105
|
+
res.setHeader('X-Frame-Options', 'DENY');
|
|
106
|
+
|
|
107
|
+
return isAllowed;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function securityMiddleware(req, res) {
|
|
111
|
+
const origin = req.headers.origin || '';
|
|
112
|
+
const corsOk = setCorsHeaders(res, origin);
|
|
113
|
+
|
|
114
|
+
if (req.method === 'OPTIONS') {
|
|
115
|
+
res.statusCode = 204;
|
|
116
|
+
res.end();
|
|
117
|
+
return { blocked: true };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!corsOk) {
|
|
121
|
+
res.statusCode = 403;
|
|
122
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
123
|
+
res.end(JSON.stringify({ error: 'Origin not allowed by CORS policy.' }));
|
|
124
|
+
return { blocked: true };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const auth = validateApiKey(req);
|
|
128
|
+
if (!auth.valid) {
|
|
129
|
+
res.statusCode = 401;
|
|
130
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
131
|
+
res.end(JSON.stringify({ error: auth.error }));
|
|
132
|
+
return { blocked: true };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { blocked: false };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function getSecurityConfig() {
|
|
139
|
+
return { ...SECURITY_CONFIG, apiKeyRequired: SECURITY_CONFIG.apiKeyRequired };
|
|
140
|
+
}
|
package/server/mcp/server.js
CHANGED
|
@@ -1,100 +1,129 @@
|
|
|
1
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import { z } from 'zod';
|
|
3
|
-
import { toolDefinitions, handleToolCall } from './tools.js';
|
|
4
|
-
import { resourceDefinitions, readResource } from './resources.js';
|
|
5
|
-
import { getPrompt } from './prompts.js';
|
|
6
|
-
|
|
7
|
-
export function createMcpServer() {
|
|
8
|
-
const server = new McpServer({
|
|
9
|
-
name: 'kiku
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
},
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
server.registerPrompt(
|
|
94
|
-
'
|
|
95
|
-
{
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { toolDefinitions, handleToolCall } from './tools.js';
|
|
4
|
+
import { resourceDefinitions, readResource } from './resources.js';
|
|
5
|
+
import { getPrompt } from './prompts.js';
|
|
6
|
+
|
|
7
|
+
export function createMcpServer() {
|
|
8
|
+
const server = new McpServer({
|
|
9
|
+
name: 'kiku',
|
|
10
|
+
title: 'kiku',
|
|
11
|
+
version: '1.1.0',
|
|
12
|
+
description: 'MCP server exposing the Ju Jingyi catalog as tools, resources, and prompts.',
|
|
13
|
+
websiteUrl: 'https://jujingyi-kikuuu.vercel.app/',
|
|
14
|
+
icons: [
|
|
15
|
+
{
|
|
16
|
+
src: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA2NCA2NCI+DQogIDxkZWZzPg0KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0icGV0YWwiIHgxPSIwIiB5MT0iMCIgeDI9IjEiIHkyPSIxIj4NCiAgICAgIDxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iI2Y5YThkNCIvPg0KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZWM0ODk5Ii8+DQogICAgPC9saW5lYXJHcmFkaWVudD4NCiAgPC9kZWZzPg0KICA8cmVjdCB3aWR0aD0iNjQiIGhlaWdodD0iNjQiIHJ4PSIxNCIgZmlsbD0iIzIwMTIyYiIvPg0KICA8ZyBmaWxsPSJ1cmwoI3BldGFsKSI+DQogICAgPGVsbGlwc2UgY3g9IjMyIiBjeT0iMTkiIHJ4PSI3LjUiIHJ5PSIxMSIvPg0KICAgIDxlbGxpcHNlIGN4PSIzMiIgY3k9IjQ1IiByeD0iNy41IiByeT0iMTEiLz4NCiAgICA8ZWxsaXBzZSBjeD0iMTkiIGN5PSIzMiIgcng9IjExIiByeT0iNy41Ii8+DQogICAgPGVsbGlwc2UgY3g9IjQ1IiBjeT0iMzIiIHJ4PSIxMSIgcnk9IjcuNSIvPg0KICA8L2c+DQogIDxjaXJjbGUgY3g9IjMyIiBjeT0iMzIiIHI9IjUuNSIgZmlsbD0iI2ZkZTY4YSIvPg0KPC9zdmc+DQo=',
|
|
17
|
+
mimeType: 'image/svg+xml',
|
|
18
|
+
sizes: ['any', '64x64', '128x128'],
|
|
19
|
+
theme: 'dark',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
src: 'https://jujingyi-kikuuu.vercel.app/mcp-logo-128.png',
|
|
23
|
+
mimeType: 'image/png',
|
|
24
|
+
sizes: ['128x128'],
|
|
25
|
+
theme: 'dark',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
src: 'https://jujingyi-kikuuu.vercel.app/mcp-logo-64.png',
|
|
29
|
+
mimeType: 'image/png',
|
|
30
|
+
sizes: ['64x64'],
|
|
31
|
+
theme: 'dark',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
src: 'https://jujingyi-kikuuu.vercel.app/mcp-logo.svg',
|
|
35
|
+
mimeType: 'image/svg+xml',
|
|
36
|
+
sizes: ['any'],
|
|
37
|
+
theme: 'dark',
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
}, {
|
|
41
|
+
capabilities: {
|
|
42
|
+
tools: {},
|
|
43
|
+
resources: {},
|
|
44
|
+
prompts: {},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
for (const tool of toolDefinitions) {
|
|
49
|
+
server.registerTool(
|
|
50
|
+
tool.name,
|
|
51
|
+
{
|
|
52
|
+
description: tool.description,
|
|
53
|
+
inputSchema: tool.inputSchema,
|
|
54
|
+
annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: false },
|
|
55
|
+
},
|
|
56
|
+
async (args) => {
|
|
57
|
+
try {
|
|
58
|
+
const result = await handleToolCall(tool.name, args);
|
|
59
|
+
return {
|
|
60
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
61
|
+
structuredContent: result,
|
|
62
|
+
isError: result?.status === 'error',
|
|
63
|
+
};
|
|
64
|
+
} catch (err) {
|
|
65
|
+
return {
|
|
66
|
+
content: [{ type: 'text', text: JSON.stringify({ status: 'error', message: err.message }) }],
|
|
67
|
+
isError: true,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const resourceMap = {
|
|
75
|
+
'kiku://bio': 'kiku-bio',
|
|
76
|
+
'kiku://awards': 'kiku-awards',
|
|
77
|
+
'kiku://catalog': 'kiku-catalog',
|
|
78
|
+
'kiku://timeline': 'kiku-timeline',
|
|
79
|
+
'kiku://discography': 'kiku-discography',
|
|
80
|
+
'kiku://brands': 'kiku-brands',
|
|
81
|
+
'kiku://social': 'kiku-social',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
for (const def of resourceDefinitions) {
|
|
85
|
+
server.registerResource(
|
|
86
|
+
resourceMap[def.uri],
|
|
87
|
+
def.uri,
|
|
88
|
+
{ description: def.description, mimeType: def.mimeType },
|
|
89
|
+
async (uri) => readResource(uri.href)
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
server.registerPrompt(
|
|
94
|
+
'recommend_series',
|
|
95
|
+
{
|
|
96
|
+
description: 'Recommend Ju Jingyi dramas based on mood or genre preference.',
|
|
97
|
+
argsSchema: {
|
|
98
|
+
mood: z.string().optional().describe('Current mood: romantic, action-packed, mysterious, emotional, or lighthearted'),
|
|
99
|
+
genre: z.string().optional().describe('Preferred genre: historical, fantasy, xianxia, romance, or suspense'),
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
async (args) => getPrompt('recommend_series', args)
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
server.registerPrompt(
|
|
106
|
+
'brand_portfolio',
|
|
107
|
+
{ description: 'Summarize Ju Jingyi brand endorsements and commercial influence.' },
|
|
108
|
+
async (args) => getPrompt('brand_portfolio', args)
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
server.registerPrompt(
|
|
112
|
+
'character_summary',
|
|
113
|
+
{
|
|
114
|
+
description: 'Get a detailed summary of a specific character role by Ju Jingyi.',
|
|
115
|
+
argsSchema: {
|
|
116
|
+
character_name: z.string().min(1).describe('Character name, such as Yang Caiwei, Bai Suzhen, Lu Wuyi, or Han Yunxi'),
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
async (args) => getPrompt('character_summary', args)
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
server.registerPrompt(
|
|
123
|
+
'career_overview',
|
|
124
|
+
{ description: 'Get a comprehensive career overview organized by era.' },
|
|
125
|
+
async (args) => getPrompt('career_overview', args)
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
return server;
|
|
129
|
+
}
|
package/server/mcp/stdio.js
CHANGED
|
@@ -10,10 +10,12 @@ let closing = false;
|
|
|
10
10
|
async function shutdown(exitCode = 0) {
|
|
11
11
|
if (closing) return;
|
|
12
12
|
closing = true;
|
|
13
|
-
await Promise.allSettled([transport.close(), server.close()]);
|
|
14
|
-
process.exitCode
|
|
13
|
+
await Promise.allSettled([transport.close().catch(() => {}), server.close().catch(() => {})]);
|
|
14
|
+
process.exit(exitCode);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
process.stdin.once('end', () => { void shutdown(0); });
|
|
18
|
+
process.stdin.once('close', () => { void shutdown(0); });
|
|
17
19
|
process.once('SIGINT', () => { void shutdown(130); });
|
|
18
20
|
process.once('SIGTERM', () => { void shutdown(143); });
|
|
19
21
|
process.once('uncaughtException', error => {
|