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
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
export const discographyData = [
|
|
2
|
-
{ id: "find-the-way", titleEn: "FIND THE WAY", titleCn: "FIND THE WAY", year: 2026, category: "single", duration: "3:30", youtubeId: "9AeErG03SZw" },
|
|
3
|
-
{ id: "set-fire", titleEn: "Set Fire to the Silent World", titleCn: "着火的沉默世界", year: 2026, category: "ost", duration: "3:55", youtubeId: "7FXGhAmHIAo", work: "Wanhua Sin" },
|
|
4
|
-
{ id: "not-suffering", titleEn: "Not Suffering", titleCn: "不苦", year: 2026, category: "ost", duration: "3:50", youtubeId: "DafSXZZo9AI", work: "Veil of Shadows" },
|
|
5
|
-
{ id: "eternal-fragrance-song", titleEn: "The Eternal Fragrance", titleCn: "千香", year: 2026, category: "ost", duration: "3:46", youtubeId: "vIanAvWb_HU", work: "The Eternal Fragrance" },
|
|
6
|
-
{ id: "rain-love", titleEn: "Yu Ai (Rain Love)", titleCn: "雨爱", year: 2026, category: "cover", duration: "4:10", youtubeId: "OMLpNyp8T7Q" },
|
|
7
|
-
{ id: "rosa", titleEn: "Rosa", titleCn: "Rosa", year: 2025, category: "single", duration: "3:20", youtubeId: "jZDzOLbd9OU" },
|
|
8
|
-
{ id: "sleepless-sunset", titleEn: "Sleepless Sunset", titleCn: "落日未眠", year: 2025, category: "single", duration: "3:45", youtubeId: "mYYD2JqZ1tE" },
|
|
9
|
-
{ id: "fall", titleEn: "Fall", titleCn: "落", year: 2024, category: "single", duration: "3:36", youtubeId: "8Nh4ZPhYKEc" },
|
|
10
|
-
{ id: "view-from-wall", titleEn: "View From The Wall", titleCn: "壁上观", year: 2024, category: "single", duration: "3:50", youtubeId: "Lge5XhVl9Fc" },
|
|
11
|
-
{ id: "ru-sha", titleEn: "Ru Sha", titleCn: "如纱", year: 2024, category: "ost", duration: "3:42", youtubeId: "lzDP2MfP3eA", work: "Sword and Fairy 4" },
|
|
12
|
-
{ id: "in-blossom-ost", titleEn: "In Blossom Main Theme", titleCn: "花间令", year: 2024, category: "ost", youtubeId: null, work: "In Blossom" },
|
|
13
|
-
{ id: "thoughts-of-joy", titleEn: "Thoughts of Joy", titleCn: "念思凡", year: 2023, category: "ost", duration: "4:05", youtubeId: "D-vEPdasbLo", work: "Beauty of Resilience" },
|
|
14
|
-
{ id: "dew-before-dawn", titleEn: "Dew Before Dawn", titleCn: "露珠在晨曦前消散", year: 2023, category: "ost", youtubeId: null, work: "Beauty of Resilience" },
|
|
15
|
-
{ id: "0.2s", titleEn: "0.2s", titleCn: "0.2s", year: 2022, category: "single", duration: "3:20", youtubeId: null },
|
|
16
|
-
{ id: "mo-li", titleEn: "Mo Li", titleCn: "莫离", year: 2021, category: "ost", duration: "4:12", youtubeId: "7VSAArsR-go", work: "Rebirth for You" },
|
|
17
|
-
{ id: "qing-cheng-shan-xia", titleEn: "Qing Cheng Shan Xia", titleCn: "青城山下", year: 2021, category: "single", youtubeId: null },
|
|
18
|
-
{ id: "fu-rong", titleEn: "Fu Rong", titleCn: "芙蓉", year: 2020, category: "ost", duration: "3:58", youtubeId: "9ke3efb9U7A", work: "The Blooms at Ruyi Pavilion" },
|
|
19
|
-
{ id: "ancient-painting", titleEn: "Ancient Painting", titleCn: "古画", year: 2020, category: "ost", youtubeId: null, work: "The Blooms at Ruyi Pavilion" },
|
|
20
|
-
{ id: "thousand-years", titleEn: "A Millennium's Wait", titleCn: "千年等一回", year: 2019, category: "ost", duration: "3:44", youtubeId: "-vf6jU96WI4", work: "The Legend of White Snake" },
|
|
21
|
-
{ id: "du-qing", titleEn: "Du Qing", titleCn: "渡情", year: 2019, category: "ost", youtubeId: null, work: "The Legend of White Snake" },
|
|
22
|
-
{ id: "wudaoying", titleEn: "Wu Dao Ying", titleCn: "舞导演", year: 2019, category: "single", youtubeId: null },
|
|
23
|
-
{ id: "gufeng-shanshui", titleEn: "Gufeng Shanshui", titleCn: "古风山水", year: 2019, category: "single", youtubeId: null },
|
|
24
|
-
{ id: "meng-lu", titleEn: "Meng Lu (Dream Road)", titleCn: "梦路", year: 2018, category: "single", youtubeId: null },
|
|
25
|
-
{ id: "zui-chi-luo", titleEn: "Zui Chi Luo", titleCn: "最赤裸", year: 2018, category: "single", youtubeId: null },
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
export const musicVideos = [
|
|
29
|
-
{ id: "mv-find-the-way", titleEn: "FIND THE WAY MV", titleCn: "FIND THE WAY", year: 2026, category: "Official MV", youtubeId: "9AeErG03SZw", duration: "3:30" },
|
|
30
|
-
{ id: "mv-not-suffering", titleEn: "Not Suffering MV", titleCn: "不苦", year: 2026, category: "OST MV", youtubeId: "DafSXZZo9AI", duration: "3:50", work: "Veil of Shadows" },
|
|
31
|
-
{ id: "mv-rain-love", titleEn: "Yu Ai (Rain Love) Stage", titleCn: "雨爱", year: 2026, category: "Stage Performance", youtubeId: "OMLpNyp8T7Q", duration: "4:10" },
|
|
32
|
-
{ id: "mv-set-fire", titleEn: "Set Fire to the Silent World MV", titleCn: "着火的沉默世界", year: 2026, category: "OST MV", youtubeId: "7FXGhAmHIAo", duration: "3:55", work: "Wanhua Sin" },
|
|
33
|
-
{ id: "mv-rosa", titleEn: "Rosa MV", titleCn: "Rosa", year: 2025, category: "Official MV", youtubeId: "jZDzOLbd9OU", duration: "3:20" },
|
|
34
|
-
{ id: "mv-fall", titleEn: "Fall MV", titleCn: "落", year: 2024, category: "Official MV", youtubeId: "8Nh4ZPhYKEc", duration: "3:36" },
|
|
35
|
-
{ id: "mv-ru-sha", titleEn: "Ru Sha MV", titleCn: "如纱", year: 2024, category: "OST MV", youtubeId: "lzDP2MfP3eA", duration: "3:42", work: "Sword and Fairy 4" },
|
|
36
|
-
];
|
|
1
|
+
export const discographyData = [
|
|
2
|
+
{ id: "find-the-way", titleEn: "FIND THE WAY", titleCn: "FIND THE WAY", year: 2026, category: "single", duration: "3:30", youtubeId: "9AeErG03SZw" },
|
|
3
|
+
{ id: "set-fire", titleEn: "Set Fire to the Silent World", titleCn: "着火的沉默世界", year: 2026, category: "ost", duration: "3:55", youtubeId: "7FXGhAmHIAo", work: "Wanhua Sin" },
|
|
4
|
+
{ id: "not-suffering", titleEn: "Not Suffering", titleCn: "不苦", year: 2026, category: "ost", duration: "3:50", youtubeId: "DafSXZZo9AI", work: "Veil of Shadows" },
|
|
5
|
+
{ id: "eternal-fragrance-song", titleEn: "The Eternal Fragrance", titleCn: "千香", year: 2026, category: "ost", duration: "3:46", youtubeId: "vIanAvWb_HU", work: "The Eternal Fragrance" },
|
|
6
|
+
{ id: "rain-love", titleEn: "Yu Ai (Rain Love)", titleCn: "雨爱", year: 2026, category: "cover", duration: "4:10", youtubeId: "OMLpNyp8T7Q" },
|
|
7
|
+
{ id: "rosa", titleEn: "Rosa", titleCn: "Rosa", year: 2025, category: "single", duration: "3:20", youtubeId: "jZDzOLbd9OU" },
|
|
8
|
+
{ id: "sleepless-sunset", titleEn: "Sleepless Sunset", titleCn: "落日未眠", year: 2025, category: "single", duration: "3:45", youtubeId: "mYYD2JqZ1tE" },
|
|
9
|
+
{ id: "fall", titleEn: "Fall", titleCn: "落", year: 2024, category: "single", duration: "3:36", youtubeId: "8Nh4ZPhYKEc" },
|
|
10
|
+
{ id: "view-from-wall", titleEn: "View From The Wall", titleCn: "壁上观", year: 2024, category: "single", duration: "3:50", youtubeId: "Lge5XhVl9Fc" },
|
|
11
|
+
{ id: "ru-sha", titleEn: "Ru Sha", titleCn: "如纱", year: 2024, category: "ost", duration: "3:42", youtubeId: "lzDP2MfP3eA", work: "Sword and Fairy 4" },
|
|
12
|
+
{ id: "in-blossom-ost", titleEn: "In Blossom Main Theme", titleCn: "花间令", year: 2024, category: "ost", youtubeId: null, work: "In Blossom" },
|
|
13
|
+
{ id: "thoughts-of-joy", titleEn: "Thoughts of Joy", titleCn: "念思凡", year: 2023, category: "ost", duration: "4:05", youtubeId: "D-vEPdasbLo", work: "Beauty of Resilience" },
|
|
14
|
+
{ id: "dew-before-dawn", titleEn: "Dew Before Dawn", titleCn: "露珠在晨曦前消散", year: 2023, category: "ost", youtubeId: null, work: "Beauty of Resilience" },
|
|
15
|
+
{ id: "0.2s", titleEn: "0.2s", titleCn: "0.2s", year: 2022, category: "single", duration: "3:20", youtubeId: null },
|
|
16
|
+
{ id: "mo-li", titleEn: "Mo Li", titleCn: "莫离", year: 2021, category: "ost", duration: "4:12", youtubeId: "7VSAArsR-go", work: "Rebirth for You" },
|
|
17
|
+
{ id: "qing-cheng-shan-xia", titleEn: "Qing Cheng Shan Xia", titleCn: "青城山下", year: 2021, category: "single", youtubeId: null },
|
|
18
|
+
{ id: "fu-rong", titleEn: "Fu Rong", titleCn: "芙蓉", year: 2020, category: "ost", duration: "3:58", youtubeId: "9ke3efb9U7A", work: "The Blooms at Ruyi Pavilion" },
|
|
19
|
+
{ id: "ancient-painting", titleEn: "Ancient Painting", titleCn: "古画", year: 2020, category: "ost", youtubeId: null, work: "The Blooms at Ruyi Pavilion" },
|
|
20
|
+
{ id: "thousand-years", titleEn: "A Millennium's Wait", titleCn: "千年等一回", year: 2019, category: "ost", duration: "3:44", youtubeId: "-vf6jU96WI4", work: "The Legend of White Snake" },
|
|
21
|
+
{ id: "du-qing", titleEn: "Du Qing", titleCn: "渡情", year: 2019, category: "ost", youtubeId: null, work: "The Legend of White Snake" },
|
|
22
|
+
{ id: "wudaoying", titleEn: "Wu Dao Ying", titleCn: "舞导演", year: 2019, category: "single", youtubeId: null },
|
|
23
|
+
{ id: "gufeng-shanshui", titleEn: "Gufeng Shanshui", titleCn: "古风山水", year: 2019, category: "single", youtubeId: null },
|
|
24
|
+
{ id: "meng-lu", titleEn: "Meng Lu (Dream Road)", titleCn: "梦路", year: 2018, category: "single", youtubeId: null },
|
|
25
|
+
{ id: "zui-chi-luo", titleEn: "Zui Chi Luo", titleCn: "最赤裸", year: 2018, category: "single", youtubeId: null },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export const musicVideos = [
|
|
29
|
+
{ id: "mv-find-the-way", titleEn: "FIND THE WAY MV", titleCn: "FIND THE WAY", year: 2026, category: "Official MV", youtubeId: "9AeErG03SZw", duration: "3:30" },
|
|
30
|
+
{ id: "mv-not-suffering", titleEn: "Not Suffering MV", titleCn: "不苦", year: 2026, category: "OST MV", youtubeId: "DafSXZZo9AI", duration: "3:50", work: "Veil of Shadows" },
|
|
31
|
+
{ id: "mv-rain-love", titleEn: "Yu Ai (Rain Love) Stage", titleCn: "雨爱", year: 2026, category: "Stage Performance", youtubeId: "OMLpNyp8T7Q", duration: "4:10" },
|
|
32
|
+
{ id: "mv-set-fire", titleEn: "Set Fire to the Silent World MV", titleCn: "着火的沉默世界", year: 2026, category: "OST MV", youtubeId: "7FXGhAmHIAo", duration: "3:55", work: "Wanhua Sin" },
|
|
33
|
+
{ id: "mv-rosa", titleEn: "Rosa MV", titleCn: "Rosa", year: 2025, category: "Official MV", youtubeId: "jZDzOLbd9OU", duration: "3:20" },
|
|
34
|
+
{ id: "mv-fall", titleEn: "Fall MV", titleCn: "落", year: 2024, category: "Official MV", youtubeId: "8Nh4ZPhYKEc", duration: "3:36" },
|
|
35
|
+
{ id: "mv-ru-sha", titleEn: "Ru Sha MV", titleCn: "如纱", year: 2024, category: "OST MV", youtubeId: "lzDP2MfP3eA", duration: "3:42", work: "Sword and Fairy 4" },
|
|
36
|
+
];
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { profileData } from '../../src/data/profileData.js';
|
|
2
|
+
import { worksData } from '../../src/data/worksData.js';
|
|
3
|
+
import { discographyData } from '../../src/data/discographyData.js';
|
|
4
|
+
import { musicVideoData } from '../../src/data/musicVideoData.js';
|
|
5
|
+
import { brandsData } from '../../src/data/brandsData.js';
|
|
6
|
+
import { socialUpdatesData } from '../../src/data/socialUpdatesData.js';
|
|
7
|
+
import { galleryData } from '../../src/data/galleryData.js';
|
|
8
|
+
import { awardsData } from '../../src/data/awardsData.js';
|
|
9
|
+
import { timelineData } from '../../src/data/timelineData.js';
|
|
10
|
+
|
|
11
|
+
export function getUnifiedProfile() {
|
|
12
|
+
return {
|
|
13
|
+
...profileData,
|
|
14
|
+
nameEn: profileData.nameEn || 'Ju Jingyi',
|
|
15
|
+
nameCn: profileData.nameCn || '鞠婧祎',
|
|
16
|
+
nameTh: profileData.nameTh || 'จวีจิ้งอี',
|
|
17
|
+
fandomName: profileData.fandomName || 'Miju (蜜橘)',
|
|
18
|
+
slogan: profileData.slogan || '鞠世无双,婧然倾心',
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getUnifiedWorks() {
|
|
23
|
+
return worksData.map(w => ({
|
|
24
|
+
id: w.id,
|
|
25
|
+
titleEn: w.titleEn || w.titleTh || w.titleCn,
|
|
26
|
+
titleCn: w.titleCn,
|
|
27
|
+
titleTh: w.titleTh,
|
|
28
|
+
year: w.year,
|
|
29
|
+
role: w.role,
|
|
30
|
+
characterName: w.role,
|
|
31
|
+
genre: w.genre || w.type || 'Drama',
|
|
32
|
+
featured: Boolean(w.featured),
|
|
33
|
+
synopsis: w.synopsis || '',
|
|
34
|
+
platform: w.platform || '',
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getUnifiedDiscography() {
|
|
39
|
+
const songs = [
|
|
40
|
+
...(discographyData.latestReleases || []),
|
|
41
|
+
...(discographyData.ostList || []),
|
|
42
|
+
...(discographyData.singles || []),
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const map = new Map();
|
|
46
|
+
for (const s of songs) {
|
|
47
|
+
const key = s.titleCn || s.titleTh || s.titleEn || s.id;
|
|
48
|
+
if (!map.has(key)) {
|
|
49
|
+
map.set(key, {
|
|
50
|
+
id: s.id,
|
|
51
|
+
titleEn: s.titleEn || s.titleTh || s.titleCn,
|
|
52
|
+
titleCn: s.titleCn,
|
|
53
|
+
titleTh: s.titleTh,
|
|
54
|
+
year: s.year,
|
|
55
|
+
type: s.type || 'Single',
|
|
56
|
+
category: s.work ? 'ost' : 'single',
|
|
57
|
+
work: s.work || null,
|
|
58
|
+
youtubeId: s.youtubeId || null,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return Array.from(map.values()).sort((a, b) => (b.year || 0) - (a.year || 0));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getUnifiedAwards() {
|
|
66
|
+
return awardsData.map(a => ({
|
|
67
|
+
id: a.id,
|
|
68
|
+
title: a.title,
|
|
69
|
+
titleCn: a.titleCn || a.title,
|
|
70
|
+
year: a.year,
|
|
71
|
+
ceremony: a.ceremony || a.organization || '',
|
|
72
|
+
category: a.category || 'General',
|
|
73
|
+
})).sort((a, b) => (b.year || 0) - (a.year || 0));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getUnifiedBrands() {
|
|
77
|
+
return brandsData.map(b => ({
|
|
78
|
+
id: b.id,
|
|
79
|
+
brandNameEn: b.brand || b.nameEn || '',
|
|
80
|
+
brandNameCn: b.brandCn || b.nameCn || b.brand || '',
|
|
81
|
+
title: b.title || 'Brand Ambassador',
|
|
82
|
+
year: b.year,
|
|
83
|
+
isGlobal: Boolean(b.isGlobal),
|
|
84
|
+
category: b.category || 'Beauty / Fashion',
|
|
85
|
+
})).sort((a, b) => (b.year || 0) - (a.year || 0));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getUnifiedTimeline() {
|
|
89
|
+
return timelineData.map(m => ({
|
|
90
|
+
id: m.id,
|
|
91
|
+
year: m.year,
|
|
92
|
+
period: m.period || String(m.year),
|
|
93
|
+
title: m.title,
|
|
94
|
+
titleCn: m.titleCn || m.title,
|
|
95
|
+
description: m.description || '',
|
|
96
|
+
era: m.era || 'Solo Career',
|
|
97
|
+
})).sort((a, b) => (b.year || 0) - (a.year || 0));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function getUnifiedGallery() {
|
|
101
|
+
return galleryData;
|
|
102
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"kiku": {
|
|
4
|
+
"type": "stdio",
|
|
5
|
+
"command": "npx",
|
|
6
|
+
"args": ["-y", "-p", "kiku-agent", "kiku-mcp"],
|
|
7
|
+
"env": {
|
|
8
|
+
"KIKU_MCP_ENABLE_WEB_TOOLS": "true"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"kiku-remote": {
|
|
12
|
+
"type": "http",
|
|
13
|
+
"url": "https://jujingyi-kikuuu.vercel.app/mcp",
|
|
14
|
+
"headers": {
|
|
15
|
+
"X-API-Key": "YOUR_API_KEY"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"kiku": {
|
|
4
|
+
"command": "npx",
|
|
5
|
+
"args": ["-y", "-p", "kiku-agent", "kiku-mcp"],
|
|
6
|
+
"env": {
|
|
7
|
+
"KIKU_MCP_ENABLE_WEB_TOOLS": "true"
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"kiku-remote": {
|
|
11
|
+
"httpUrl": "https://jujingyi-kikuuu.vercel.app/mcp",
|
|
12
|
+
"headers": {
|
|
13
|
+
"X-API-Key": "YOUR_API_KEY"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://opencode.ai/config.json",
|
|
3
|
+
"mcp": {
|
|
4
|
+
"kiku": {
|
|
5
|
+
"type": "local",
|
|
6
|
+
"command": ["npx", "-y", "-p", "kiku-agent", "kiku-mcp"],
|
|
7
|
+
"environment": {
|
|
8
|
+
"KIKU_MCP_ENABLE_WEB_TOOLS": "true"
|
|
9
|
+
},
|
|
10
|
+
"enabled": true
|
|
11
|
+
},
|
|
12
|
+
"kiku-remote": {
|
|
13
|
+
"type": "remote",
|
|
14
|
+
"url": "https://jujingyi-kikuuu.vercel.app/mcp",
|
|
15
|
+
"headers": {
|
|
16
|
+
"X-API-Key": "YOUR_API_KEY"
|
|
17
|
+
},
|
|
18
|
+
"enabled": true
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"servers": {
|
|
3
|
+
"kiku": {
|
|
4
|
+
"type": "stdio",
|
|
5
|
+
"command": "npx",
|
|
6
|
+
"args": ["-y", "-p", "kiku-agent", "kiku-mcp"],
|
|
7
|
+
"env": {
|
|
8
|
+
"KIKU_MCP_ENABLE_WEB_TOOLS": "true"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"kiku-remote": {
|
|
12
|
+
"type": "http",
|
|
13
|
+
"url": "https://jujingyi-kikuuu.vercel.app/mcp",
|
|
14
|
+
"headers": {
|
|
15
|
+
"X-API-Key": "YOUR_API_KEY"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
package/server/mcp/handler.js
CHANGED
|
@@ -1,159 +1,85 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function
|
|
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
|
-
const
|
|
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
|
-
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
87
|
-
const sessionId = url.searchParams.get('sessionId');
|
|
88
|
-
if (!sessionId || !validateSessionId(sessionId)) {
|
|
89
|
-
return sendJson(res, 400, { error: 'Invalid or missing sessionId.' });
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const session = sessions.get(sessionId);
|
|
93
|
-
if (!session) {
|
|
94
|
-
return sendJson(res, 404, { error: 'Session not found or expired. Reconnect through /api/v1/mcp/sse.' });
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const rate = checkRateLimit(`mcp:${sessionId}`);
|
|
98
|
-
if (!rate.allowed) {
|
|
99
|
-
return sendJson(res, 429, { error: rate.error }, { 'Retry-After': '60' });
|
|
100
|
-
}
|
|
101
|
-
res.setHeader('X-RateLimit-Remaining', String(rate.remaining));
|
|
102
|
-
|
|
103
|
-
session.messageCount++;
|
|
104
|
-
session.lastSeen = Date.now();
|
|
105
|
-
if (session.messageCount > 500) {
|
|
106
|
-
await closeSession(sessionId);
|
|
107
|
-
return sendJson(res, 429, { error: 'Session message limit reached. Reconnect to create a new session.' });
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
await session.transport.handlePostMessage(req, res);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export async function handleMcpHttp(req, res) {
|
|
114
|
-
const security = securityMiddleware(req, res);
|
|
115
|
-
if (security.blocked || !validateRequestSize(req, res)) return;
|
|
116
|
-
|
|
117
|
-
if (req.method !== 'POST') {
|
|
118
|
-
res.setHeader('Allow', 'POST, OPTIONS');
|
|
119
|
-
return sendJson(res, 405, {
|
|
120
|
-
jsonrpc: '2.0',
|
|
121
|
-
error: { code: -32000, message: 'Method not allowed. Use POST for Streamable HTTP.' },
|
|
122
|
-
id: null,
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const rate = checkRateLimit(`mcp-http:${req.socket?.remoteAddress || 'unknown'}`);
|
|
127
|
-
if (!rate.allowed) return sendJson(res, 429, { error: rate.error }, { 'Retry-After': '60' });
|
|
128
|
-
res.setHeader('X-RateLimit-Remaining', String(rate.remaining));
|
|
129
|
-
|
|
130
|
-
const server = createMcpServer();
|
|
131
|
-
const transport = new StreamableHTTPServerTransport({
|
|
132
|
-
sessionIdGenerator: undefined,
|
|
133
|
-
enableJsonResponse: true,
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
await server.connect(transport);
|
|
138
|
-
await transport.handleRequest(req, res);
|
|
139
|
-
} catch (error) {
|
|
140
|
-
console.error(`MCP Streamable HTTP: ${error.message}`);
|
|
141
|
-
sendJson(res, 500, {
|
|
142
|
-
jsonrpc: '2.0',
|
|
143
|
-
error: { code: -32603, message: 'Internal MCP server error.' },
|
|
144
|
-
id: null,
|
|
145
|
-
});
|
|
146
|
-
} finally {
|
|
147
|
-
await Promise.allSettled([transport.close(), server.close()]);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export function getMcpSessionStats() {
|
|
152
|
-
return { activeSessions: sessions.size, maxSessions: MAX_SESSIONS, ttlMs: SESSION_TTL_MS };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export function isMcpRoute(pathname) {
|
|
156
|
-
return pathname === '/api/v1/mcp'
|
|
157
|
-
|| pathname === '/api/v1/mcp/sse'
|
|
158
|
-
|| pathname === '/api/v1/mcp/message';
|
|
159
|
-
}
|
|
1
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
2
|
+
import { createMcpServer } from './server.js';
|
|
3
|
+
import { checkRateLimit, securityMiddleware, MAX_REQUEST_BODY_SIZE } from './security.js';
|
|
4
|
+
|
|
5
|
+
function sendJson(res, statusCode, payload, headers = {}) {
|
|
6
|
+
if (res.headersSent) return;
|
|
7
|
+
res.statusCode = statusCode;
|
|
8
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
9
|
+
for (const [name, value] of Object.entries(headers)) res.setHeader(name, value);
|
|
10
|
+
res.end(JSON.stringify(payload));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function validateRequestSize(req, res) {
|
|
14
|
+
const contentLength = Number.parseInt(req.headers['content-length'] || '0', 10);
|
|
15
|
+
if (Number.isFinite(contentLength) && contentLength > MAX_REQUEST_BODY_SIZE) {
|
|
16
|
+
sendJson(res, 413, { error: 'Request body too large.' }, { 'Retry-After': '60' });
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function handleMcpSse(req, res) {
|
|
23
|
+
sendJson(res, 410, { error: 'Legacy SSE transport removed. Use POST /mcp.' });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function handleMcpMessage(req, res) {
|
|
27
|
+
sendJson(res, 410, { error: 'Legacy SSE transport removed. Use POST /mcp.' });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function handleMcpHttp(req, res) {
|
|
31
|
+
const security = securityMiddleware(req, res);
|
|
32
|
+
if (security.blocked || !validateRequestSize(req, res)) return;
|
|
33
|
+
|
|
34
|
+
if (req.method === 'OPTIONS') {
|
|
35
|
+
res.statusCode = 204;
|
|
36
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
37
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
38
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With, X-API-Key, Accept');
|
|
39
|
+
res.end();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (req.method !== 'POST') {
|
|
44
|
+
res.setHeader('Allow', 'GET, POST, OPTIONS');
|
|
45
|
+
return sendJson(res, 405, {
|
|
46
|
+
jsonrpc: '2.0',
|
|
47
|
+
error: { code: -32000, message: 'Method not allowed. Use POST for Streamable HTTP JSON-RPC, or GET /mcp for server discovery.' },
|
|
48
|
+
id: null,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const rate = checkRateLimit(`mcp-http:${req.socket?.remoteAddress || 'unknown'}`);
|
|
53
|
+
if (!rate.allowed) return sendJson(res, 429, { error: rate.error }, { 'Retry-After': '60' });
|
|
54
|
+
res.setHeader('X-RateLimit-Remaining', String(rate.remaining));
|
|
55
|
+
|
|
56
|
+
const server = createMcpServer();
|
|
57
|
+
const transport = new StreamableHTTPServerTransport({
|
|
58
|
+
sessionIdGenerator: undefined,
|
|
59
|
+
enableJsonResponse: true,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
await server.connect(transport);
|
|
64
|
+
await transport.handleRequest(req, res);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error(`MCP Streamable HTTP: ${error.message}`);
|
|
67
|
+
sendJson(res, 500, {
|
|
68
|
+
jsonrpc: '2.0',
|
|
69
|
+
error: { code: -32603, message: 'Internal MCP server error.' },
|
|
70
|
+
id: null,
|
|
71
|
+
});
|
|
72
|
+
} finally {
|
|
73
|
+
await Promise.allSettled([transport.close(), server.close()]);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getMcpSessionStats() {
|
|
78
|
+
return { mode: 'stateless', transport: 'streamable-http' };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function isMcpRoute(pathname) {
|
|
82
|
+
return pathname === '/mcp'
|
|
83
|
+
|| pathname === '/mcp/sse'
|
|
84
|
+
|| pathname === '/mcp/message';
|
|
85
|
+
}
|