kiku-agent 1.3.2 → 1.3.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 +69 -12
- package/package.json +2 -1
- package/server/agent/agentCore.js +319 -46
- package/server/agent/appPaths.js +31 -0
- package/server/agent/audioPlayer.js +339 -12
- package/server/agent/autoContext.js +193 -0
- package/server/agent/catalogEntityResolver.js +170 -28
- package/server/agent/clarifyingQuestions.js +137 -35
- package/server/agent/cli.js +177 -48
- package/server/agent/cloudAdapters.js +116 -0
- package/server/agent/commandRegistry.js +409 -80
- package/server/agent/commands.js +1056 -125
- package/server/agent/config.js +1088 -690
- package/server/agent/contextCompactor.js +213 -21
- package/server/agent/cpuMetrics.js +199 -0
- package/server/agent/engineService.js +591 -0
- package/server/agent/eval/modules/evaluation/runner.js +4 -14
- package/server/agent/eval/modules/evaluation/scoring.js +15 -3
- package/server/agent/eval/modules/server/evalApi.js +5 -5
- package/server/agent/eval/modules/ui/evalUi.js +7 -4
- package/server/agent/eventBus.js +1 -0
- package/server/agent/ink/main.jsx +196 -191
- package/server/agent/jobManager.js +11 -1
- package/server/agent/lanDiscovery.js +721 -23
- package/server/agent/llmClient.js +317 -71
- package/server/agent/lmStudioWakeup.js +517 -65
- package/server/agent/locationService.js +797 -0
- package/server/agent/mcpQuizData.js +509 -0
- package/server/agent/memory/contextAllocator.js +19 -7
- package/server/agent/memory.js +3 -1
- package/server/agent/modelCompatibility.js +352 -0
- package/server/agent/modelDefaults.js +1 -0
- package/server/agent/modelManager.js +46 -2
- package/server/agent/newsWatch.js +9 -3
- package/server/agent/processManager.js +229 -0
- package/server/agent/prompts.js +174 -8
- package/server/agent/routingInterceptor.js +9 -1
- package/server/agent/sanitizer.js +41 -6
- package/server/agent/searchQueryPlanner.js +202 -0
- package/server/agent/session.js +59 -4
- package/server/agent/tests/agentFollowUps.test.js +128 -2
- package/server/agent/tests/agentMusicControl.test.js +245 -0
- package/server/agent/tests/apiRouter.test.js +1 -1
- package/server/agent/tests/autoContext.test.js +176 -0
- package/server/agent/tests/bilibiliEmbed.test.js +45 -0
- package/server/agent/tests/chatHardening.test.js +121 -0
- package/server/agent/tests/chineseWebSearchEscalation.test.js +62 -0
- package/server/agent/tests/clarifyingQuestions.test.js +33 -0
- package/server/agent/tests/cliAutomation.test.js +38 -1
- package/server/agent/tests/cloudProviders.test.js +91 -3
- package/server/agent/tests/commands.test.js +92 -3
- package/server/agent/tests/contextIntelligence.test.js +50 -3
- package/server/agent/tests/cpuMetrics.test.js +101 -0
- package/server/agent/tests/decisionAndSearchEnhancements.test.js +32 -0
- package/server/agent/tests/edgeCasesScenarios.test.js +82 -0
- package/server/agent/tests/engineService.test.js +436 -0
- package/server/agent/tests/engineUnload.test.js +94 -0
- package/server/agent/tests/envCommand.test.js +69 -0
- package/server/agent/tests/evalMetrics.test.js +8 -0
- package/server/agent/tests/fetchApi.test.js +99 -0
- package/server/agent/tests/hybridWebSearch.test.js +2 -0
- package/server/agent/tests/lanDiscovery.test.js +5 -0
- package/server/agent/tests/lanInterfaces.test.js +510 -0
- package/server/agent/tests/lmStudioWakeup.test.js +278 -0
- package/server/agent/tests/locationTool.test.js +278 -0
- package/server/agent/tests/memoryRoute.test.js +173 -0
- package/server/agent/tests/memoryUiNormalization.test.js +75 -0
- package/server/agent/tests/modelCompatibility.test.js +148 -0
- package/server/agent/tests/modelManager.test.js +32 -0
- package/server/agent/tests/musicControlUiBridge.test.js +113 -0
- package/server/agent/tests/nextGenArchitecture.test.js +4 -2
- package/server/agent/tests/openDomainPrompt.test.js +197 -0
- package/server/agent/tests/openDomainSearch.test.js +121 -0
- package/server/agent/tests/processManager.test.js +144 -0
- package/server/agent/tests/providerPersistence.test.js +10 -5
- package/server/agent/tests/quickActionsAndModals.test.js +180 -0
- package/server/agent/tests/searchQueryPlanner.test.js +108 -0
- package/server/agent/tests/securityAuditRemediation.test.js +16 -9
- package/server/agent/tests/securityAuditRemediations2.test.js +154 -0
- package/server/agent/tests/securityHardening.test.js +4 -4
- package/server/agent/tests/sessionAndCopy.test.js +39 -4
- package/server/agent/tests/sidebarControls.test.js +34 -0
- package/server/agent/tests/statefulMemory.test.js +8 -1
- package/server/agent/tests/textProtocolLoop.test.js +71 -0
- package/server/agent/tests/textToolParsing.test.js +48 -0
- package/server/agent/tests/toolRoutingRegressions.test.js +41 -0
- package/server/agent/tests/unifiedEndpoints.test.js +13 -6
- package/server/agent/tests/v110Features.test.js +1 -1
- package/server/agent/tests/vramEstimator.test.js +75 -0
- package/server/agent/tests/webChatDocs.test.js +199 -0
- package/server/agent/tests/webChatInferenceControl.test.js +298 -0
- package/server/agent/tests/webChatInspection.test.js +256 -5
- package/server/agent/tests/webChatMcp.test.js +94 -0
- package/server/agent/tests/webExtract.test.js +23 -0
- package/server/agent/tests/webSources.test.js +46 -12
- package/server/agent/textToolProtocol.js +173 -46
- package/server/agent/toolRouter.js +318 -44
- package/server/agent/tools.js +877 -96
- package/server/agent/ui.js +39 -34
- package/server/agent/vramService.js +35 -54
- package/server/agent/webSearch.js +441 -162
- package/server/agent/websearch/orchestrator.js +15 -5
- package/server/agent/websearch/resilience/cache.js +9 -2
- package/server/agent/websearch/security/ssrf.js +41 -10
- package/server/agent/youtubeAuth.js +5 -20
- package/server/apiRouter.js +5558 -4422
- package/server/mcp/data/discography.js +2 -2
- package/server/mcp/examples/antigravity.example.json +17 -0
- package/server/mcp/handler.js +8 -4
- package/server/mcp/security.js +220 -21
- package/server/mcp/server.js +2 -1
- package/server/mcp/tests/connection.test.js +14 -8
- package/server/mcp/tests/mcpKeyAuth.test.js +156 -0
- package/server/mcp/tools.js +7 -1
- package/server/supabase_migration_prefix.sql +102 -0
- package/server/supabase_schema.sql +18 -18
- package/server/web-chat/core/connection.js +179 -218
- package/server/web-chat/core/conversationContext.js +9 -2
- package/server/web-chat/core/directCatalogEngine.js +52 -28
- package/server/web-chat/core/dramaKnowledge.js +33 -22
- package/server/web-chat/core/followUpEngine.js +735 -81
- package/server/web-chat/core/futureEventGuard.js +5 -1
- package/server/web-chat/core/musicControlAction.js +70 -0
- package/server/web-chat/core/security.js +32 -21
- package/server/web-chat/core/toolInspector.js +6 -6
- package/server/web-chat/core/urlGuard.js +45 -0
- package/server/web-chat/router.js +204 -157
- package/server/web-chat/routes/chatRoute.js +885 -488
- package/server/web-chat/routes/engineRoute.js +270 -0
- package/server/web-chat/routes/exportRoute.js +1 -1
- package/server/web-chat/routes/lanDiscoveryRoute.js +17 -2
- package/server/web-chat/routes/lmStudioLoadRoute.js +172 -0
- package/server/web-chat/routes/mcpRoute.js +386 -0
- package/server/web-chat/routes/memoryRoute.js +253 -0
- package/server/web-chat/routes/providerRoute.js +49 -16
- package/server/web-chat/routes/staticRoute.js +306 -17
- package/server/web-chat/routes/statusRoute.js +194 -73
- package/server/web-chat/routes/suggestRoute.js +11 -3
- package/server/web-chat/routes/verifyRoute.js +26 -4
- package/server/web-chat/server.js +7 -6
- package/server/web-chat/ui/docsBuilder.js +3558 -0
- package/server/web-chat/ui/htmlBuilder.js +12 -0
- package/server/web-chat/ui/scripts/client.js +3401 -1302
- package/server/web-chat/ui/scripts/webchatI18n.js +155 -153
- package/server/web-chat/ui/settingsBuilder.js +7870 -0
- package/server/web-chat/ui/styles/lan-card.css +165 -0
- package/server/web-chat/ui/styles/main.css +4289 -447
- package/server/web-chat/ui/sw.js +64 -0
- package/server/web-chat/ui/templates/body.html +407 -281
- package/src/data/awardsData.js +146 -146
- package/src/data/musicVideoData.js +6 -4
- package/src/lib/vram/estimator.js +195 -19
|
@@ -12,7 +12,7 @@ export const discographyData = [
|
|
|
12
12
|
{ id: "stay-with-me", titleEn: "Stay With Me", titleCn: "伴", year: 2024, category: "ost", duration: "3:30", youtubeId: "N7pWFylNQDM", work: "I Want to Go Against the Wind" },
|
|
13
13
|
{ id: "wu-chang-sheng", titleEn: "Longevity By Mistake", titleCn: "误长生", year: 2023, category: "ost", duration: "3:50", youtubeId: "GSZqFPkBMws", work: "Beauty of Resilience" },
|
|
14
14
|
{ id: "reng-xun", titleEn: "Still Searching", titleCn: "仍寻", year: 2023, category: "ost", duration: "5:01", youtubeId: "nsTL6g_UOqE", work: "Beauty of Resilience" },
|
|
15
|
-
{ id: "dew-before-dawn", titleEn: "Dew Before Dawn (Dewdrops)", titleCn: "露珠在晨曦前消散", year: 2026, category: "ost", duration: "4:42", youtubeId: "D-vEPdasbLo", work: "Veil of Shadows", aliases: ["Dewdrops", "Dewdrops Dissipate Before Dawn", "Dew Before Dawn", "露珠在晨曦前消散"] },
|
|
15
|
+
{ id: "dew-before-dawn", titleEn: "Dew Before Dawn (Dewdrops)", titleCn: "露珠在晨曦前消散", year: 2026, category: "ost", duration: "4:42", youtubeId: "D-vEPdasbLo", work: "Veil of Shadows / In Blossom", aliases: ["Dewdrops", "Dewdrops Dissipate Before Dawn", "Dew Before Dawn", "露珠在晨曦前消散", "In Blossom", "花间令", "露水"] },
|
|
16
16
|
{ id: "tu-hei-su", titleEn: "Melatonin", titleCn: "褪黑素", year: 2024, category: "stage", duration: "3:22", youtubeId: "1t0E5bVqGIQ" },
|
|
17
17
|
{ id: "you-shan-lian", titleEn: "You Shan Lian", titleCn: "游山恋", year: 2024, category: "stage", duration: "4:06", youtubeId: "ZaGQuDsN0pc" },
|
|
18
18
|
{ id: "mo-nu-da-mao-xian", titleEn: "The Witch's Great Adventure", titleCn: "魔女大冒险", year: 2024, category: "stage", duration: "5:59", youtubeId: "rzn_nqmBV-M" },
|
|
@@ -29,7 +29,7 @@ export const discographyData = [
|
|
|
29
29
|
{ id: "thousand-years", titleEn: "A Millennium's Wait", titleCn: "千年等一回", year: 2019, category: "ost", duration: "4:05", youtubeId: "-vf6jU96WI4", work: "The Legend of White Snake" },
|
|
30
30
|
{ id: "du-qing", titleEn: "Du Qing", titleCn: "渡情", year: 2019, category: "ost", duration: "3:30", youtubeId: "1dmp3hUhVBI", work: "The Legend of White Snake" },
|
|
31
31
|
{ id: "qing-cheng-shan-xia", titleEn: "Bai Suzhen under Mount Qingcheng", titleCn: "青城山下白素贞", year: 2019, category: "ost", duration: "3:22", youtubeId: "wVZbHrI0q24", work: "The Legend of White Snake" },
|
|
32
|
-
{ id: "love-emergency", titleEn: "Love Emergency", titleCn: "恋爱告急", year: 2019, category: "single", duration: "3:35", youtubeId: "5MLPdCmnlmE" },
|
|
32
|
+
{ id: "love-emergency", titleEn: "Love Emergency", titleCn: "恋爱告急", year: 2019, category: "single", duration: "3:35", youtubeId: "5MLPdCmnlmE", aliases: ["Heartbeat Alert", "Love Emergency", "恋爱告急"] },
|
|
33
33
|
{ id: "tan-yun-xi", titleEn: "Sigh of Yunxi", titleCn: "叹云兮", year: 2018, category: "ost", duration: "3:50", youtubeId: "oEgxqij58LQ", work: "Legend of Yunxi" },
|
|
34
34
|
{ id: "falling-flowers", titleEn: "Falling Flowers", titleCn: "落花成泥", year: 2018, category: "ost", duration: "3:42", youtubeId: "cWibu5ISTyw", work: "Legend of Yunxi" },
|
|
35
35
|
{ id: "hong-zhao-yuan", titleEn: "Hong Zhao Yuan", titleCn: "红昭愿", year: 2018, category: "stage", duration: "3:30", youtubeId: "6gFUB9eDFD0" },
|
|
@@ -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
|
+
"url": "https://jujingyi-kikuuu.vercel.app/mcp",
|
|
12
|
+
"headers": {
|
|
13
|
+
"X-API-Key": "YOUR_API_KEY"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
package/server/mcp/handler.js
CHANGED
|
@@ -28,7 +28,7 @@ export async function handleMcpMessage(req, res) {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export async function handleMcpHttp(req, res) {
|
|
31
|
-
const security = securityMiddleware(req, res);
|
|
31
|
+
const security = await securityMiddleware(req, res);
|
|
32
32
|
if (security.blocked || !validateRequestSize(req, res)) return;
|
|
33
33
|
|
|
34
34
|
if (req.method !== 'POST') {
|
|
@@ -40,12 +40,16 @@ export async function handleMcpHttp(req, res) {
|
|
|
40
40
|
});
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
const
|
|
43
|
+
const auth = security.auth || await validateApiKey(req);
|
|
44
|
+
const rateBucket = auth.keyPrefix ? `mcp-key:${auth.keyPrefix}` : `mcp-http:${req.socket?.remoteAddress || 'unknown'}`;
|
|
45
|
+
const maxLimit = auth.rateLimit || 30;
|
|
46
|
+
const rate = checkRateLimit(rateBucket, maxLimit);
|
|
44
47
|
if (!rate.allowed) return sendJson(res, 429, { error: rate.error }, { 'Retry-After': '60' });
|
|
48
|
+
res.setHeader('X-RateLimit-Limit', String(maxLimit));
|
|
45
49
|
res.setHeader('X-RateLimit-Remaining', String(rate.remaining));
|
|
46
50
|
|
|
47
|
-
const authorized =
|
|
48
|
-
const server = createMcpServer({ transport: 'http', authorized });
|
|
51
|
+
const authorized = auth.valid && auth.authorized !== false;
|
|
52
|
+
const server = createMcpServer({ transport: 'http', authorized, tier: auth.tier });
|
|
49
53
|
const transport = new StreamableHTTPServerTransport({
|
|
50
54
|
sessionIdGenerator: undefined,
|
|
51
55
|
enableJsonResponse: true,
|
package/server/mcp/security.js
CHANGED
|
@@ -1,7 +1,106 @@
|
|
|
1
|
-
import { timingSafeEqual } from 'node:crypto';
|
|
1
|
+
import { timingSafeEqual, createHmac, createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
function getSupabaseConfig() {
|
|
4
|
+
const url = process.env.SUPABASE_URL || '';
|
|
5
|
+
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY || '';
|
|
6
|
+
const anonKey = process.env.SUPABASE_ANON_KEY || '';
|
|
7
|
+
const isConfigured = Boolean(url && (serviceKey || anonKey));
|
|
8
|
+
return { url, serviceKey, anonKey, isConfigured };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const API_KEY_PEPPER = process.env.API_KEY_PEPPER || process.env.KIKU_KEY_PEPPER || 'kiku_mcp_pepper_secret_2026';
|
|
12
|
+
|
|
13
|
+
function hashApiKey(rawKey) {
|
|
14
|
+
if (!rawKey || typeof rawKey !== 'string') return '';
|
|
15
|
+
return createHmac('sha256', API_KEY_PEPPER).update(rawKey.trim()).digest('hex');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function hashApiKeyLegacy(rawKey) {
|
|
19
|
+
if (!rawKey || typeof rawKey !== 'string') return '';
|
|
20
|
+
return createHash('sha256').update(rawKey.trim()).digest('hex');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function safeCompare(a, b) {
|
|
24
|
+
if (!a || !b || typeof a !== 'string' || typeof b !== 'string') return false;
|
|
25
|
+
const bufA = Buffer.from(a);
|
|
26
|
+
const bufB = Buffer.from(b);
|
|
27
|
+
if (bufA.length !== bufB.length) return false;
|
|
28
|
+
return timingSafeEqual(bufA, bufB);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const mcpApiKeyCache = new Map();
|
|
32
|
+
const MCP_API_KEY_CACHE_MAX = 5000;
|
|
33
|
+
|
|
34
|
+
export function setMcpApiKeyCache(hash, entry) {
|
|
35
|
+
if (mcpApiKeyCache.size >= MCP_API_KEY_CACHE_MAX) {
|
|
36
|
+
const oldest = mcpApiKeyCache.keys().next().value;
|
|
37
|
+
if (oldest) mcpApiKeyCache.delete(oldest);
|
|
38
|
+
}
|
|
39
|
+
mcpApiKeyCache.set(hash, entry);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const dynamicMcpKeysStore = new Map();
|
|
43
|
+
|
|
44
|
+
export async function saveMcpApiKeyToSupabase({ apiKey, tier = 'mcp_vip', rateLimit = 1000 }) {
|
|
45
|
+
const hash = hashApiKey(apiKey);
|
|
46
|
+
const keyPrefix = apiKey.slice(0, 16);
|
|
47
|
+
const entry = { apiKey, tier, rateLimit, isActive: true, validUntil: Date.now() + 300000, keyPrefix };
|
|
48
|
+
setMcpApiKeyCache(hash, entry);
|
|
49
|
+
dynamicMcpKeysStore.set(apiKey, { tier, rateLimit, keyPrefix });
|
|
50
|
+
|
|
51
|
+
const { url, serviceKey, isConfigured } = getSupabaseConfig();
|
|
52
|
+
if (!isConfigured || !serviceKey) {
|
|
53
|
+
return { stored: 'memory', keyPrefix, hash };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
const timeoutId = setTimeout(() => controller.abort(), 3000);
|
|
59
|
+
const payload = JSON.stringify({
|
|
60
|
+
key_prefix: keyPrefix,
|
|
61
|
+
key_hash: hash,
|
|
62
|
+
tier,
|
|
63
|
+
rate_limit_per_min: rateLimit,
|
|
64
|
+
is_active: true
|
|
65
|
+
});
|
|
66
|
+
const headers = {
|
|
67
|
+
'apikey': serviceKey,
|
|
68
|
+
'Authorization': `Bearer ${serviceKey}`,
|
|
69
|
+
'Content-Type': 'application/json',
|
|
70
|
+
'Prefer': 'return=representation'
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
let res = await fetch(`${url}/rest/v1/jujingyi_kikuuu_api_keys`, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers,
|
|
76
|
+
body: payload,
|
|
77
|
+
signal: controller.signal
|
|
78
|
+
});
|
|
79
|
+
if (res.status === 404) {
|
|
80
|
+
res = await fetch(`${url}/rest/v1/api_keys`, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers,
|
|
83
|
+
body: payload,
|
|
84
|
+
signal: controller.signal
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
clearTimeout(timeoutId);
|
|
88
|
+
return { stored: res.ok ? 'supabase' : 'memory', keyPrefix, hash };
|
|
89
|
+
} catch (err) {
|
|
90
|
+
return { stored: 'memory', keyPrefix, hash, error: err.message };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
2
93
|
|
|
3
94
|
const SECURITY_CONFIG = {
|
|
4
|
-
apiKeyRequired
|
|
95
|
+
get apiKeyRequired() {
|
|
96
|
+
return Boolean(
|
|
97
|
+
process.env.MCP_API_KEY ||
|
|
98
|
+
process.env.MCP_API_KEYS ||
|
|
99
|
+
process.env.KIKU_MASTER_KEYS ||
|
|
100
|
+
process.env.KIKU_MASTER_KEY ||
|
|
101
|
+
process.env.NODE_ENV === 'production'
|
|
102
|
+
);
|
|
103
|
+
},
|
|
5
104
|
|
|
6
105
|
rateLimitWindowMs: 60_000,
|
|
7
106
|
rateLimitMaxRequests: 30,
|
|
@@ -16,9 +115,7 @@ const SECURITY_CONFIG = {
|
|
|
16
115
|
|
|
17
116
|
export const MAX_REQUEST_BODY_SIZE = SECURITY_CONFIG.maxRequestBodyBytes;
|
|
18
117
|
|
|
19
|
-
export function validateApiKey(req) {
|
|
20
|
-
if (!SECURITY_CONFIG.apiKeyRequired) return { valid: true };
|
|
21
|
-
|
|
118
|
+
export async function validateApiKey(req) {
|
|
22
119
|
let key = req?.headers?.['x-api-key'];
|
|
23
120
|
if (!key) {
|
|
24
121
|
const authHeader = req?.headers?.['authorization'];
|
|
@@ -26,35 +123,132 @@ export function validateApiKey(req) {
|
|
|
26
123
|
key = authHeader.slice(7).trim();
|
|
27
124
|
}
|
|
28
125
|
}
|
|
29
|
-
|
|
30
|
-
|
|
126
|
+
|
|
127
|
+
if (!key) {
|
|
128
|
+
if (!SECURITY_CONFIG.apiKeyRequired) {
|
|
129
|
+
return { valid: true, authorized: false, tier: 'anonymous', rateLimit: SECURITY_CONFIG.rateLimitMaxRequests };
|
|
130
|
+
}
|
|
131
|
+
return { valid: false, error: 'Missing API key. Set X-API-Key header or Authorization: Bearer <key>.' };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const trimmed = String(key).trim();
|
|
135
|
+
|
|
136
|
+
if (process.env.MCP_API_KEY && safeCompare(trimmed, process.env.MCP_API_KEY.trim())) {
|
|
137
|
+
return { valid: true, authorized: true, tier: 'mcp_vip', rateLimit: 1000, keyPrefix: 'mcp_env' };
|
|
31
138
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
139
|
+
|
|
140
|
+
if (process.env.MCP_API_KEYS) {
|
|
141
|
+
const envKeys = process.env.MCP_API_KEYS.split(',').map(s => s.trim()).filter(Boolean);
|
|
142
|
+
for (const ek of envKeys) {
|
|
143
|
+
if (safeCompare(trimmed, ek)) {
|
|
144
|
+
return { valid: true, authorized: true, tier: 'mcp_vip', rateLimit: 1000, keyPrefix: 'mcp_env' };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
const masterKeys = [
|
|
151
|
+
...(process.env.KIKU_MASTER_KEYS || '').split(',').map(s => s.trim()).filter(Boolean),
|
|
152
|
+
...(process.env.KIKU_MASTER_KEY ? [process.env.KIKU_MASTER_KEY.trim()] : [])
|
|
153
|
+
];
|
|
154
|
+
for (const mk of masterKeys) {
|
|
155
|
+
if (safeCompare(trimmed, mk)) {
|
|
156
|
+
return { valid: true, authorized: true, tier: 'master_vip', rateLimit: 2000, keyPrefix: 'kiku_master' };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
const memoryKey = dynamicMcpKeysStore.get(trimmed);
|
|
162
|
+
if (memoryKey) {
|
|
163
|
+
return {
|
|
164
|
+
valid: true,
|
|
165
|
+
authorized: true,
|
|
166
|
+
tier: memoryKey.tier || 'mcp_vip',
|
|
167
|
+
rateLimit: memoryKey.rateLimit || 1000,
|
|
168
|
+
keyPrefix: memoryKey.keyPrefix || trimmed.slice(0, 16)
|
|
169
|
+
};
|
|
37
170
|
}
|
|
38
|
-
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
const hash = hashApiKey(trimmed);
|
|
174
|
+
const legacyHash = hashApiKeyLegacy(trimmed);
|
|
175
|
+
const cached = mcpApiKeyCache.get(hash) || mcpApiKeyCache.get(legacyHash);
|
|
176
|
+
if (cached && Date.now() < cached.validUntil) {
|
|
177
|
+
if (cached.isActive) {
|
|
178
|
+
return {
|
|
179
|
+
valid: true,
|
|
180
|
+
authorized: true,
|
|
181
|
+
tier: cached.tier || 'mcp_vip',
|
|
182
|
+
rateLimit: cached.rateLimit || 1000,
|
|
183
|
+
keyPrefix: cached.keyPrefix || trimmed.slice(0, 16)
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
return { valid: false, error: 'API key has been revoked or expired.' };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
const { url, serviceKey, anonKey, isConfigured } = getSupabaseConfig();
|
|
191
|
+
if (isConfigured) {
|
|
192
|
+
try {
|
|
193
|
+
const apiKeyHeader = serviceKey || anonKey;
|
|
194
|
+
const controller = new AbortController();
|
|
195
|
+
const timeoutId = setTimeout(() => controller.abort(), 2500);
|
|
196
|
+
const headers = {
|
|
197
|
+
'apikey': apiKeyHeader,
|
|
198
|
+
'Authorization': `Bearer ${apiKeyHeader}`,
|
|
199
|
+
'Content-Type': 'application/json'
|
|
200
|
+
};
|
|
201
|
+
let res = await fetch(`${url}/rest/v1/jujingyi_kikuuu_api_keys?or=(key_hash.eq.${hash},key_hash.eq.${legacyHash})&is_active=eq.true&select=tier,rate_limit_per_min,expires_at`, {
|
|
202
|
+
headers,
|
|
203
|
+
signal: controller.signal
|
|
204
|
+
});
|
|
205
|
+
if (res.status === 404) {
|
|
206
|
+
res = await fetch(`${url}/rest/v1/api_keys?or=(key_hash.eq.${hash},key_hash.eq.${legacyHash})&is_active=eq.true&select=tier,rate_limit_per_min,expires_at`, {
|
|
207
|
+
headers,
|
|
208
|
+
signal: controller.signal
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
clearTimeout(timeoutId);
|
|
212
|
+
|
|
213
|
+
if (res.ok) {
|
|
214
|
+
const rows = await res.json();
|
|
215
|
+
if (Array.isArray(rows) && rows.length > 0) {
|
|
216
|
+
const row = rows[0];
|
|
217
|
+
if (row.expires_at && new Date(row.expires_at).getTime() < Date.now()) {
|
|
218
|
+
setMcpApiKeyCache(hash, { isActive: false, validUntil: Date.now() + 60000 });
|
|
219
|
+
return { valid: false, error: 'API key has expired.' };
|
|
220
|
+
}
|
|
221
|
+
const tier = row.tier || 'mcp_vip';
|
|
222
|
+
const rateLimit = Number(row.rate_limit_per_min) || 1000;
|
|
223
|
+
const keyPrefix = trimmed.slice(0, 16);
|
|
224
|
+
const result = { valid: true, authorized: true, tier, rateLimit, keyPrefix };
|
|
225
|
+
setMcpApiKeyCache(hash, { ...result, isActive: true, validUntil: Date.now() + 300000 });
|
|
226
|
+
return result;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
} catch {}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return { valid: false, error: 'Invalid API key.' };
|
|
39
233
|
}
|
|
40
234
|
|
|
41
235
|
const rateLimitStore = new Map();
|
|
42
236
|
|
|
43
|
-
export function checkRateLimit(sessionId) {
|
|
237
|
+
export function checkRateLimit(sessionId, maxRequests = SECURITY_CONFIG.rateLimitMaxRequests) {
|
|
44
238
|
const now = Date.now();
|
|
45
239
|
const entry = rateLimitStore.get(sessionId);
|
|
46
240
|
|
|
47
241
|
if (!entry || now - entry.windowStart > SECURITY_CONFIG.rateLimitWindowMs) {
|
|
48
242
|
rateLimitStore.set(sessionId, { count: 1, windowStart: now });
|
|
49
|
-
return { allowed: true, remaining:
|
|
243
|
+
return { allowed: true, remaining: maxRequests - 1, limit: maxRequests };
|
|
50
244
|
}
|
|
51
245
|
|
|
52
246
|
entry.count++;
|
|
53
|
-
const remaining =
|
|
247
|
+
const remaining = maxRequests - entry.count;
|
|
54
248
|
if (remaining < 0) {
|
|
55
|
-
return { allowed: false, error: `Rate limit exceeded. Max ${
|
|
249
|
+
return { allowed: false, remaining: 0, limit: maxRequests, error: `Rate limit exceeded. Max ${maxRequests} requests per minute.` };
|
|
56
250
|
}
|
|
57
|
-
return { allowed: true, remaining };
|
|
251
|
+
return { allowed: true, remaining, limit: maxRequests };
|
|
58
252
|
}
|
|
59
253
|
|
|
60
254
|
const rateLimitCleanupTimer = setInterval(() => {
|
|
@@ -64,6 +258,11 @@ const rateLimitCleanupTimer = setInterval(() => {
|
|
|
64
258
|
rateLimitStore.delete(id);
|
|
65
259
|
}
|
|
66
260
|
}
|
|
261
|
+
for (const [hash, entry] of mcpApiKeyCache) {
|
|
262
|
+
if (entry.validUntil && entry.validUntil < now) {
|
|
263
|
+
mcpApiKeyCache.delete(hash);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
67
266
|
}, 60_000);
|
|
68
267
|
rateLimitCleanupTimer.unref?.();
|
|
69
268
|
|
|
@@ -121,7 +320,7 @@ export function setCorsHeaders(res, origin) {
|
|
|
121
320
|
return isAllowed;
|
|
122
321
|
}
|
|
123
322
|
|
|
124
|
-
export function securityMiddleware(req, res) {
|
|
323
|
+
export async function securityMiddleware(req, res) {
|
|
125
324
|
const origin = req.headers.origin || '';
|
|
126
325
|
const corsOk = setCorsHeaders(res, origin);
|
|
127
326
|
|
|
@@ -138,7 +337,7 @@ export function securityMiddleware(req, res) {
|
|
|
138
337
|
return { blocked: true };
|
|
139
338
|
}
|
|
140
339
|
|
|
141
|
-
const auth = validateApiKey(req);
|
|
340
|
+
const auth = await validateApiKey(req);
|
|
142
341
|
if (!auth.valid) {
|
|
143
342
|
res.statusCode = 401;
|
|
144
343
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
@@ -146,7 +345,7 @@ export function securityMiddleware(req, res) {
|
|
|
146
345
|
return { blocked: true };
|
|
147
346
|
}
|
|
148
347
|
|
|
149
|
-
return { blocked: false };
|
|
348
|
+
return { blocked: false, auth };
|
|
150
349
|
}
|
|
151
350
|
|
|
152
351
|
export function getSecurityConfig() {
|
package/server/mcp/server.js
CHANGED
|
@@ -9,13 +9,14 @@ const WRITE_TOOLS = new Set([
|
|
|
9
9
|
'manage_skill',
|
|
10
10
|
'play_music', 'stop_music', 'pause_music', 'resume_music',
|
|
11
11
|
'mute_music', 'unmute_music', 'next_track', 'prev_track',
|
|
12
|
+
'set_volume', 'seek_music', 'set_repeat_mode', 'set_shuffle_mode', 'toggle_video_mode',
|
|
12
13
|
'add_to_queue', 'remove_from_queue', 'clear_queue',
|
|
13
14
|
]);
|
|
14
15
|
|
|
15
|
-
|
|
16
16
|
const NON_IDEMPOTENT_TOOLS = new Set([
|
|
17
17
|
'manage_skill', 'play_music', 'add_to_queue', 'remove_from_queue',
|
|
18
18
|
'clear_queue', 'next_track', 'prev_track', 'run_quiz',
|
|
19
|
+
'set_volume', 'seek_music', 'set_repeat_mode', 'set_shuffle_mode', 'toggle_video_mode',
|
|
19
20
|
]);
|
|
20
21
|
|
|
21
22
|
const OPEN_WORLD_TOOLS = new Set([
|
|
@@ -8,8 +8,8 @@ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
|
|
8
8
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
9
9
|
import { handleMcpHttp } from '../handler.js';
|
|
10
10
|
import sseHandler from '../../../api/mcp/sse.js';
|
|
11
|
-
import messageHandler from '../../../api/mcp/message.js';
|
|
12
|
-
import { PACKAGE_VERSION } from '../../agent/version.js';
|
|
11
|
+
import messageHandler from '../../../api/mcp/message.js';
|
|
12
|
+
import { PACKAGE_VERSION } from '../../agent/version.js';
|
|
13
13
|
|
|
14
14
|
const stdioServerPath = fileURLToPath(new URL('../stdio.js', import.meta.url));
|
|
15
15
|
|
|
@@ -20,7 +20,7 @@ test('MCP client connects over stdio and exercises tools, resources, and prompts
|
|
|
20
20
|
cwd: process.cwd(),
|
|
21
21
|
stderr: 'pipe',
|
|
22
22
|
});
|
|
23
|
-
const client = new Client({ name: 'kiku-mcp-integration-test', version: PACKAGE_VERSION }, { capabilities: {} });
|
|
23
|
+
const client = new Client({ name: 'kiku-mcp-integration-test', version: PACKAGE_VERSION }, { capabilities: {} });
|
|
24
24
|
|
|
25
25
|
try {
|
|
26
26
|
await client.connect(transport);
|
|
@@ -62,6 +62,8 @@ test('MCP client connects over stdio and exercises tools, resources, and prompts
|
|
|
62
62
|
});
|
|
63
63
|
|
|
64
64
|
test('MCP client connects through the real Streamable HTTP handler at /mcp', { timeout: 30_000 }, async () => {
|
|
65
|
+
const previousApiKey = process.env.MCP_API_KEY;
|
|
66
|
+
process.env.MCP_API_KEY = 'kiku-mcp-test-key';
|
|
65
67
|
const { handleApiRequest } = await import('../../apiRouter.js');
|
|
66
68
|
const httpServer = createServer((req, res) => {
|
|
67
69
|
void handleApiRequest(req, res).catch(error => {
|
|
@@ -80,15 +82,17 @@ test('MCP client connects through the real Streamable HTTP handler at /mcp', { t
|
|
|
80
82
|
const discoveryRes = await fetch(`${baseUrl}/mcp`);
|
|
81
83
|
assert.equal(discoveryRes.status, 200);
|
|
82
84
|
const discoveryData = await discoveryRes.json();
|
|
83
|
-
assert.equal(discoveryData.status, 'success');
|
|
84
|
-
assert.equal(discoveryData.name, 'kiku');
|
|
85
|
-
assert.equal(discoveryData.version, PACKAGE_VERSION);
|
|
85
|
+
assert.equal(discoveryData.status, 'success');
|
|
86
|
+
assert.equal(discoveryData.name, 'kiku');
|
|
87
|
+
assert.equal(discoveryData.version, PACKAGE_VERSION);
|
|
86
88
|
assert.equal(discoveryData.endpoints.http, `${baseUrl}/mcp`);
|
|
87
89
|
assert.equal(discoveryData.endpoints.sse, undefined);
|
|
88
90
|
assert.equal(discoveryData.endpoints.message, undefined);
|
|
89
91
|
|
|
90
|
-
const transport = new StreamableHTTPClientTransport(new URL(`${baseUrl}/mcp`)
|
|
91
|
-
|
|
92
|
+
const transport = new StreamableHTTPClientTransport(new URL(`${baseUrl}/mcp`), {
|
|
93
|
+
requestInit: { headers: { 'X-API-Key': 'kiku-mcp-test-key' } },
|
|
94
|
+
});
|
|
95
|
+
const client = new Client({ name: 'kiku-http-mcp-test', version: PACKAGE_VERSION }, { capabilities: {} });
|
|
92
96
|
|
|
93
97
|
try {
|
|
94
98
|
await client.connect(transport);
|
|
@@ -101,6 +105,8 @@ test('MCP client connects through the real Streamable HTTP handler at /mcp', { t
|
|
|
101
105
|
} finally {
|
|
102
106
|
await client.close().catch(() => {});
|
|
103
107
|
await new Promise(resolve => httpServer.close(resolve));
|
|
108
|
+
if (previousApiKey === undefined) delete process.env.MCP_API_KEY;
|
|
109
|
+
else process.env.MCP_API_KEY = previousApiKey;
|
|
104
110
|
}
|
|
105
111
|
});
|
|
106
112
|
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { createServer } from 'node:http';
|
|
4
|
+
|
|
5
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
6
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
7
|
+
import { PACKAGE_VERSION } from '../../agent/version.js';
|
|
8
|
+
import { handleApiRequest } from '../../apiRouter.js';
|
|
9
|
+
import { MCP_QUIZ_CORRECT_ANSWERS } from '../../agent/mcpQuizData.js';
|
|
10
|
+
|
|
11
|
+
test('Dynamic MCP API Key flow: challenge -> speedhack guard -> 12-question quiz claim -> Streamable HTTP authentication', { timeout: 35_000 }, async () => {
|
|
12
|
+
const previousEnvKey = process.env.MCP_API_KEY;
|
|
13
|
+
process.env.MCP_API_KEY = 'mcp-static-fallback-key';
|
|
14
|
+
|
|
15
|
+
const httpServer = createServer((req, res) => {
|
|
16
|
+
void handleApiRequest(req, res).catch(error => {
|
|
17
|
+
res.statusCode = 500;
|
|
18
|
+
res.end(error.message);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
await new Promise((resolve, reject) => {
|
|
23
|
+
httpServer.once('error', reject);
|
|
24
|
+
httpServer.listen(0, '127.0.0.1', resolve);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const address = httpServer.address();
|
|
28
|
+
const baseUrl = `http://127.0.0.1:${address.port}`;
|
|
29
|
+
|
|
30
|
+
let client = null;
|
|
31
|
+
let transport = null;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
|
|
35
|
+
const challengeRes = await fetch(`${baseUrl}/api/v1/mcp/quiz/challenge`);
|
|
36
|
+
assert.equal(challengeRes.status, 200);
|
|
37
|
+
const challengeJson = await challengeRes.json();
|
|
38
|
+
assert.equal(challengeJson.status, 'success');
|
|
39
|
+
assert.equal(challengeJson.data.totalQuestions, 12);
|
|
40
|
+
assert.equal(challengeJson.data.requiredScore, 12);
|
|
41
|
+
const challengeToken = challengeJson.data.challengeToken;
|
|
42
|
+
assert.ok(challengeToken && challengeToken.startsWith('kiku_mcp_chal_'));
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
const failNoTokenRes = await fetch(`${baseUrl}/api/v1/mcp/keys/claim`, {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: { 'Content-Type': 'application/json' },
|
|
48
|
+
body: JSON.stringify({ answers: [] })
|
|
49
|
+
});
|
|
50
|
+
assert.equal(failNoTokenRes.status, 403);
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
const correctAnswers = MCP_QUIZ_CORRECT_ANSWERS.map((ans, idx) => ({
|
|
54
|
+
qIndex: idx,
|
|
55
|
+
answerIndex: ans
|
|
56
|
+
}));
|
|
57
|
+
const speedhackRes = await fetch(`${baseUrl}/api/v1/mcp/keys/claim`, {
|
|
58
|
+
method: 'POST',
|
|
59
|
+
headers: { 'Content-Type': 'application/json' },
|
|
60
|
+
body: JSON.stringify({
|
|
61
|
+
challengeToken,
|
|
62
|
+
answers: correctAnswers
|
|
63
|
+
})
|
|
64
|
+
});
|
|
65
|
+
assert.equal(speedhackRes.status, 403);
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
await new Promise(r => setTimeout(r, 2600));
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
const wrongAnswers = MCP_QUIZ_CORRECT_ANSWERS.map((ans, idx) => ({
|
|
72
|
+
qIndex: idx,
|
|
73
|
+
answerIndex: 99
|
|
74
|
+
}));
|
|
75
|
+
const failWrongAnsRes = await fetch(`${baseUrl}/api/v1/mcp/keys/claim`, {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: { 'Content-Type': 'application/json' },
|
|
78
|
+
body: JSON.stringify({
|
|
79
|
+
challengeToken,
|
|
80
|
+
answers: wrongAnswers
|
|
81
|
+
})
|
|
82
|
+
});
|
|
83
|
+
assert.equal(failWrongAnsRes.status, 403);
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
const claimRes = await fetch(`${baseUrl}/api/v1/mcp/keys/claim`, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: { 'Content-Type': 'application/json' },
|
|
89
|
+
body: JSON.stringify({
|
|
90
|
+
challengeToken,
|
|
91
|
+
answers: correctAnswers
|
|
92
|
+
})
|
|
93
|
+
});
|
|
94
|
+
assert.equal(claimRes.status, 200);
|
|
95
|
+
const claimJson = await claimRes.json();
|
|
96
|
+
assert.equal(claimJson.status, 'success');
|
|
97
|
+
assert.equal(claimJson.data.tier, 'MCP VIP Tier');
|
|
98
|
+
assert.equal(claimJson.data.rateLimit, 1000);
|
|
99
|
+
const claimedKey = claimJson.data.apiKey;
|
|
100
|
+
assert.ok(claimedKey.startsWith('kiku_mcp_sk_'));
|
|
101
|
+
assert.ok(claimJson.data.claudeConfig?.mcpServers?.['kiku-remote']);
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
const replayRes = await fetch(`${baseUrl}/api/v1/mcp/keys/claim`, {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: { 'Content-Type': 'application/json' },
|
|
107
|
+
body: JSON.stringify({
|
|
108
|
+
challengeToken,
|
|
109
|
+
answers: correctAnswers
|
|
110
|
+
})
|
|
111
|
+
});
|
|
112
|
+
assert.equal(replayRes.status, 403);
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
transport = new StreamableHTTPClientTransport(new URL(`${baseUrl}/mcp`), {
|
|
116
|
+
requestInit: { headers: { 'X-API-Key': claimedKey } },
|
|
117
|
+
});
|
|
118
|
+
client = new Client({ name: 'kiku-dynamic-mcp-key-test', version: PACKAGE_VERSION }, { capabilities: {} });
|
|
119
|
+
await client.connect(transport);
|
|
120
|
+
|
|
121
|
+
const tools = await client.listTools();
|
|
122
|
+
assert.ok(tools.tools.length >= 10);
|
|
123
|
+
assert.ok(tools.tools.some(t => t.name === 'search_works'));
|
|
124
|
+
|
|
125
|
+
const callResult = await client.callTool({
|
|
126
|
+
name: 'search_works',
|
|
127
|
+
arguments: { year: 2026 }
|
|
128
|
+
});
|
|
129
|
+
assert.equal(callResult.isError, false);
|
|
130
|
+
const parsedContent = JSON.parse(callResult.content[0].text);
|
|
131
|
+
assert.equal(parsedContent.status, 'success');
|
|
132
|
+
assert.ok(parsedContent.count > 0);
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
const badKeyRes = await fetch(`${baseUrl}/mcp`, {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: {
|
|
138
|
+
'Content-Type': 'application/json',
|
|
139
|
+
'X-API-Key': 'kiku_mcp_sk_invalid_bogus_key'
|
|
140
|
+
},
|
|
141
|
+
body: JSON.stringify({
|
|
142
|
+
jsonrpc: '2.0',
|
|
143
|
+
method: 'initialize',
|
|
144
|
+
params: { protocolVersion: '2024-11-05', clientInfo: { name: 'bad-client', version: '1.0.0' } },
|
|
145
|
+
id: 1
|
|
146
|
+
})
|
|
147
|
+
});
|
|
148
|
+
assert.equal(badKeyRes.status, 401);
|
|
149
|
+
|
|
150
|
+
} finally {
|
|
151
|
+
process.env.MCP_API_KEY = previousEnvKey;
|
|
152
|
+
if (client) await client.close().catch(() => {});
|
|
153
|
+
if (transport) await transport.close().catch(() => {});
|
|
154
|
+
await new Promise(resolve => httpServer.close(resolve));
|
|
155
|
+
}
|
|
156
|
+
});
|
package/server/mcp/tools.js
CHANGED
|
@@ -19,6 +19,7 @@ const PUBLIC_TOOL_NAMES = new Set([
|
|
|
19
19
|
'global_search',
|
|
20
20
|
'get_system_stats',
|
|
21
21
|
'get_current_datetime',
|
|
22
|
+
'get_user_location',
|
|
22
23
|
'get_youtube_transcript',
|
|
23
24
|
'search_news',
|
|
24
25
|
'fetch_rss',
|
|
@@ -32,7 +33,7 @@ const OPTIONAL_WEB_TOOL_NAMES = new Set(['web_search', 'fetch_webpage', 'fetch_a
|
|
|
32
33
|
const webToolsEnabled = process.env.KIKU_MCP_ENABLE_WEB_TOOLS === 'true';
|
|
33
34
|
|
|
34
35
|
const OPTIONAL_CN_TOOL_NAMES = new Set(['search_chinese_web', 'search_bilibili', 'search_web_images']);
|
|
35
|
-
const cnToolsEnabled = process.env.KIKU_ENABLE_CN_WEB_TOOLS
|
|
36
|
+
const cnToolsEnabled = !['false', '0', 'off', 'no'].includes((process.env.KIKU_ENABLE_CN_WEB_TOOLS ?? '').trim().toLowerCase());
|
|
36
37
|
|
|
37
38
|
const MUSIC_TOOL_NAMES = new Set([
|
|
38
39
|
'search_youtube',
|
|
@@ -60,6 +61,11 @@ const PRIVILEGED_TOOL_NAMES = new Set([
|
|
|
60
61
|
'resume_music',
|
|
61
62
|
'next_track',
|
|
62
63
|
'prev_track',
|
|
64
|
+
'set_volume',
|
|
65
|
+
'seek_music',
|
|
66
|
+
'set_repeat_mode',
|
|
67
|
+
'set_shuffle_mode',
|
|
68
|
+
'toggle_video_mode',
|
|
63
69
|
'add_to_queue',
|
|
64
70
|
'remove_from_queue',
|
|
65
71
|
'clear_queue',
|