sanook-cli 0.4.0 → 0.5.0
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/.env.example +19 -0
- package/CHANGELOG.md +144 -0
- package/README.md +153 -20
- package/README.th.md +136 -0
- package/dist/agentContext.js +4 -0
- package/dist/approval.js +6 -0
- package/dist/bin.js +394 -51
- package/dist/brain.js +92 -59
- package/dist/brand.js +47 -0
- package/dist/checkpoint.js +37 -0
- package/dist/commands.js +86 -6
- package/dist/compaction.js +76 -5
- package/dist/config.js +100 -12
- package/dist/cost.js +60 -3
- package/dist/doctor.js +92 -0
- package/dist/gateway/auth.js +2 -2
- package/dist/gateway/ledger.js +2 -2
- package/dist/gateway/scheduler.js +1 -0
- package/dist/gateway/serve.js +6 -4
- package/dist/gateway/server.js +10 -2
- package/dist/git.js +11 -2
- package/dist/hooks.js +43 -17
- package/dist/knowledge.js +48 -49
- package/dist/loop.js +182 -66
- package/dist/lsp/client.js +173 -0
- package/dist/lsp/framing.js +56 -0
- package/dist/lsp/index.js +138 -0
- package/dist/lsp/servers.js +82 -0
- package/dist/mcp-server.js +244 -0
- package/dist/mcp.js +184 -29
- package/dist/memory-store.js +559 -0
- package/dist/memory.js +143 -29
- package/dist/orchestrate.js +150 -0
- package/dist/providers/codex.js +2 -2
- package/dist/providers/keys.js +3 -2
- package/dist/providers/registry.js +133 -1
- package/dist/repomap.js +93 -0
- package/dist/search/chunk.js +158 -0
- package/dist/search/embed-store.js +187 -0
- package/dist/search/engine.js +203 -0
- package/dist/search/fuse.js +35 -0
- package/dist/search/index-core.js +187 -0
- package/dist/search/indexer.js +241 -0
- package/dist/search/store.js +77 -0
- package/dist/session.js +42 -8
- package/dist/skill-install.js +10 -10
- package/dist/skills.js +12 -9
- package/dist/summarize.js +31 -0
- package/dist/tools/bash.js +21 -2
- package/dist/tools/diagnostics.js +41 -0
- package/dist/tools/edit.js +29 -7
- package/dist/tools/index.js +8 -1
- package/dist/tools/list.js +7 -2
- package/dist/tools/permission.js +90 -9
- package/dist/tools/read.js +23 -4
- package/dist/tools/remember.js +1 -1
- package/dist/tools/sandbox.js +61 -0
- package/dist/tools/search.js +105 -4
- package/dist/tools/task.js +195 -29
- package/dist/tools/timeout.js +35 -0
- package/dist/tools/util.js +10 -0
- package/dist/tools/write.js +6 -4
- package/dist/trust.js +89 -0
- package/dist/ui/app.js +218 -27
- package/dist/ui/banner.js +4 -9
- package/dist/ui/history.js +30 -0
- package/dist/ui/mentions.js +44 -0
- package/dist/ui/setup.js +6 -5
- package/dist/ui/useEditor.js +83 -0
- package/dist/update.js +114 -0
- package/dist/worktree.js +173 -0
- package/package.json +11 -5
- package/scripts/postinstall.mjs +33 -0
- package/second-brain/.agents/_Index.md +30 -0
- package/second-brain/.agents/skills/_Index.md +30 -0
- package/second-brain/.agents/workflows/_Index.md +30 -0
- package/second-brain/AGENTS.md +4 -4
- package/second-brain/Acceptance/_Index.md +30 -0
- package/second-brain/Acceptance/golden-case-template.md +39 -0
- package/second-brain/Areas/_Index.md +30 -0
- package/second-brain/Bugs/System-OS/_Index.md +30 -0
- package/second-brain/Bugs/_Index.md +30 -0
- package/second-brain/CLAUDE.md +4 -1
- package/second-brain/Checklists/_Index.md +30 -0
- package/second-brain/Checklists/preflight-postflight-template.md +29 -0
- package/second-brain/Distillations/_Index.md +30 -0
- package/second-brain/Entities/_Index.md +30 -0
- package/second-brain/Entities/entity-template.md +33 -0
- package/second-brain/Evals/_Index.md +30 -0
- package/second-brain/Evals/correction-pairs.md +24 -0
- package/second-brain/Evals/failure-taxonomy.md +24 -0
- package/second-brain/Evals/golden-set.md +25 -0
- package/second-brain/Evals/quality-ledger.md +23 -0
- package/second-brain/Evals/self-eval-rubric.md +23 -0
- package/second-brain/GEMINI.md +4 -4
- package/second-brain/Goals/_Index.md +30 -0
- package/second-brain/Handoffs/_Index.md +30 -0
- package/second-brain/Home.md +7 -0
- package/second-brain/Intake/Raw Sources/_Index.md +30 -0
- package/second-brain/Intake/_Index.md +30 -0
- package/second-brain/Intake/_Quarantine/_Index.md +30 -0
- package/second-brain/Learning/_Index.md +30 -0
- package/second-brain/Playbooks/_Index.md +30 -0
- package/second-brain/Playbooks/playbook-template.md +23 -0
- package/second-brain/Projects/_Index.md +30 -0
- package/second-brain/Prompts/_Index.md +30 -0
- package/second-brain/README.md +2 -1
- package/second-brain/Research/_Index.md +30 -0
- package/second-brain/Retrospectives/_Index.md +30 -0
- package/second-brain/Reviews/_Index.md +30 -0
- package/second-brain/Runbooks/_Index.md +30 -0
- package/second-brain/Runbooks/eval-loop.md +24 -0
- package/second-brain/Sessions/_Index.md +30 -0
- package/second-brain/Shared/AI-Context-Index.md +20 -0
- package/second-brain/Shared/AI-Threads/_Index.md +30 -0
- package/second-brain/Shared/Archive/_Index.md +30 -0
- package/second-brain/Shared/Assets/_Index.md +30 -0
- package/second-brain/Shared/Context-Packs/_Index.md +30 -0
- package/second-brain/Shared/Context7-Docs/_Index.md +30 -0
- package/second-brain/Shared/Coordination/NOW.md +28 -0
- package/second-brain/Shared/Coordination/_Index.md +30 -0
- package/second-brain/Shared/Coordination/agent-registry.md +24 -0
- package/second-brain/Shared/Coordination/task-board/_Index.md +30 -0
- package/second-brain/Shared/Coordination/task-board/task-template.md +43 -0
- package/second-brain/Shared/Coordination/task-board.md +32 -0
- package/second-brain/Shared/Core-Facts/_Index.md +30 -0
- package/second-brain/Shared/Decision-Memory/_Index.md +30 -0
- package/second-brain/Shared/Glossary/_Index.md +30 -0
- package/second-brain/Shared/Memory-Inbox/_Index.md +30 -0
- package/second-brain/Shared/Operating-State/_Index.md +30 -0
- package/second-brain/Shared/Prompting/_Index.md +30 -0
- package/second-brain/Shared/Provenance/_Index.md +30 -0
- package/second-brain/Shared/Rules/_Index.md +30 -0
- package/second-brain/Shared/Rules/contextual-note-rule.md +30 -0
- package/second-brain/Shared/Rules/frontmatter-standard.md +10 -0
- package/second-brain/Shared/Rules/memory-write-protocol.md +28 -0
- package/second-brain/Shared/Rules/procedural-runbook-header.md +40 -0
- package/second-brain/Shared/Rules/review-and-staleness-policy.md +22 -0
- package/second-brain/Shared/Rules/rules-formatting.md +34 -0
- package/second-brain/Shared/Scripts/_Index.md +30 -0
- package/second-brain/Shared/Scripts-Archive/_Index.md +30 -0
- package/second-brain/Shared/Tech-Standards/_Index.md +30 -0
- package/second-brain/Shared/Tech-Standards/verification-standard.md +40 -0
- package/second-brain/Shared/User-Memory/_Index.md +30 -0
- package/second-brain/Shared/User-Persona/_Index.md +30 -0
- package/second-brain/Shared/User-Persona/owner-profile.md +25 -0
- package/second-brain/Shared/Working-Memory/_Index.md +30 -0
- package/second-brain/Shared/_Index.md +30 -0
- package/second-brain/Shared/mcp-servers/_Index.md +30 -0
- package/second-brain/Skills/_Index.md +30 -0
- package/second-brain/Templates/_Index.md +30 -0
- package/second-brain/Templates/bug.md +2 -0
- package/second-brain/Templates/handoff.md +2 -0
- package/second-brain/Templates/session.md +2 -0
- package/second-brain/Tools/_Index.md +30 -0
- package/second-brain/Traces/_Index.md +30 -0
- package/second-brain/Vault Structure Map.md +33 -1
- package/second-brain/copilot/_Index.md +30 -0
- package/skills/audit-license-compliance/SKILL.md +117 -0
- package/skills/author-codemod/SKILL.md +110 -0
- package/skills/build-audit-logging/SKILL.md +112 -0
- package/skills/build-cdc-streaming-pipeline/SKILL.md +123 -0
- package/skills/build-cli-tool/SKILL.md +108 -0
- package/skills/build-data-table/SKILL.md +141 -0
- package/skills/build-native-mobile-ui/SKILL.md +154 -0
- package/skills/build-offline-first-sync/SKILL.md +118 -0
- package/skills/build-realtime-channel/SKILL.md +122 -0
- package/skills/build-vector-search/SKILL.md +131 -0
- package/skills/compose-local-dev-stack/SKILL.md +149 -0
- package/skills/configure-bundler-build/SKILL.md +166 -0
- package/skills/configure-dns-tls/SKILL.md +142 -0
- package/skills/configure-reverse-proxy-lb/SKILL.md +129 -0
- package/skills/configure-security-headers-csp/SKILL.md +122 -0
- package/skills/contract-testing/SKILL.md +140 -0
- package/skills/datetime-timezone-correctness/SKILL.md +125 -0
- package/skills/debug-ci-pipeline-failure/SKILL.md +134 -0
- package/skills/debug-flaky-tests/SKILL.md +128 -0
- package/skills/defend-llm-prompt-injection/SKILL.md +110 -0
- package/skills/deliver-webhooks/SKILL.md +116 -0
- package/skills/design-api-pagination/SKILL.md +144 -0
- package/skills/design-authorization-model/SKILL.md +119 -0
- package/skills/design-backup-dr-recovery/SKILL.md +113 -0
- package/skills/design-event-sourcing-cqrs/SKILL.md +143 -0
- package/skills/design-multi-tenancy/SKILL.md +100 -0
- package/skills/design-protobuf-grpc-service/SKILL.md +146 -0
- package/skills/design-relational-schema/SKILL.md +129 -0
- package/skills/design-search-index-infra/SKILL.md +151 -0
- package/skills/design-state-machine/SKILL.md +108 -0
- package/skills/design-token-system/SKILL.md +109 -0
- package/skills/distributed-locks-leases/SKILL.md +120 -0
- package/skills/encrypt-sensitive-data/SKILL.md +148 -0
- package/skills/feature-flags-rollout/SKILL.md +130 -0
- package/skills/file-upload-object-storage/SKILL.md +107 -0
- package/skills/fuzz-dynamic-security-test/SKILL.md +111 -0
- package/skills/harden-llm-app-reliability/SKILL.md +126 -0
- package/skills/i18n-localization-setup/SKILL.md +113 -0
- package/skills/idempotency-keys/SKILL.md +107 -0
- package/skills/implement-push-notifications/SKILL.md +142 -0
- package/skills/ingest-webhook-secure/SKILL.md +120 -0
- package/skills/integrate-oauth-oidc/SKILL.md +126 -0
- package/skills/load-stress-test/SKILL.md +129 -0
- package/skills/map-privacy-data-gdpr/SKILL.md +146 -0
- package/skills/model-nosql-data/SKILL.md +118 -0
- package/skills/money-decimal-arithmetic/SKILL.md +123 -0
- package/skills/monitor-ml-drift/SKILL.md +109 -0
- package/skills/numeric-precision-units/SKILL.md +144 -0
- package/skills/optimize-llm-cost-latency/SKILL.md +103 -0
- package/skills/optimize-react-rerenders/SKILL.md +124 -0
- package/skills/orchestrate-agent-workflow/SKILL.md +100 -0
- package/skills/payments-billing-integration/SKILL.md +114 -0
- package/skills/pin-toolchain-versions/SKILL.md +116 -0
- package/skills/plan-strangler-migration/SKILL.md +95 -0
- package/skills/property-based-testing/SKILL.md +108 -0
- package/skills/publish-package-registry/SKILL.md +130 -0
- package/skills/recover-git-state/SKILL.md +119 -0
- package/skills/remediate-web-vulnerabilities/SKILL.md +125 -0
- package/skills/resilience-timeouts-retries/SKILL.md +104 -0
- package/skills/resolve-merge-rebase-conflict/SKILL.md +97 -0
- package/skills/rewrite-git-history/SKILL.md +109 -0
- package/skills/scaffold-cross-platform-app/SKILL.md +137 -0
- package/skills/schema-evolution-compatibility/SKILL.md +121 -0
- package/skills/send-transactional-email/SKILL.md +126 -0
- package/skills/serve-deploy-ml-model/SKILL.md +107 -0
- package/skills/setup-cdn-edge-waf/SKILL.md +107 -0
- package/skills/setup-devcontainer-env/SKILL.md +131 -0
- package/skills/setup-lint-format-precommit/SKILL.md +140 -0
- package/skills/setup-monorepo-tooling/SKILL.md +125 -0
- package/skills/ship-mobile-app-store-release/SKILL.md +137 -0
- package/skills/structured-output-llm/SKILL.md +86 -0
- package/skills/supply-chain-sbom-provenance/SKILL.md +120 -0
- package/skills/test-data-factories/SKILL.md +158 -0
- package/skills/threat-model-stride/SKILL.md +123 -0
- package/skills/train-evaluate-ml-model/SKILL.md +109 -0
- package/skills/unicode-text-correctness/SKILL.md +109 -0
- package/skills/visual-regression-testing/SKILL.md +120 -0
package/dist/mcp.js
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
3
|
import { readFileSync } from 'node:fs';
|
|
6
4
|
import { dynamicTool, jsonSchema } from 'ai';
|
|
5
|
+
import { appHomePath, appProjectPath, BRAND } from './brand.js';
|
|
6
|
+
import { hasUntrustedProjectConfig, projectConfigPathIfTrusted, projectRoot } from './trust.js';
|
|
7
7
|
// version จาก package.json (single source of truth) — กัน drift เหมือน bin.ts/banner
|
|
8
8
|
const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
9
|
-
// MCP client
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
|
|
9
|
+
// MCP client เขียนเอง zero-dep — ต่อ MCP server (filesystem/github/postgres/ฯลฯ)
|
|
10
|
+
// 2 transport: stdio (command) + Streamable-HTTP (url) → ต่อทั้ง local และ remote/hosted MCP ได้
|
|
11
|
+
// config: ~/.sanook/mcp.json + project .sanook/mcp.json
|
|
12
|
+
// stdio: { "fs": { "command": "npx", "args": ["-y","@modelcontextprotocol/server-filesystem","/path"] } }
|
|
13
|
+
// remote: { "gh": { "url": "https://api.example.com/mcp", "headers": { "Authorization": "Bearer …" } } }
|
|
14
|
+
export const PROTOCOL_VERSION = '2024-11-05'; // shared by the MCP client (here) and server (mcp-server.ts)
|
|
13
15
|
const MAX_BUF = 16 * 1024 * 1024; // กัน server ส่ง byte ยาวไม่มี newline → memory โต unbounded
|
|
16
|
+
const REQUEST_TIMEOUT = 20_000;
|
|
14
17
|
// env ปลอดภัยที่ส่งให้ MCP child (ไม่มี secret) — server ที่ต้อง token ให้ตั้งใน cfg.env เอง
|
|
15
18
|
const SAFE_ENV_KEYS = ['PATH', 'HOME', 'TMPDIR', 'TEMP', 'LANG', 'LC_ALL', 'USER', 'SHELL', 'TERM', 'NODE_PATH', 'NVM_DIR', 'APPDATA'];
|
|
16
19
|
function safeEnv() {
|
|
@@ -22,8 +25,12 @@ function safeEnv() {
|
|
|
22
25
|
}
|
|
23
26
|
return out;
|
|
24
27
|
}
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
export function isValidMcpServerName(name) {
|
|
29
|
+
return (/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(name) &&
|
|
30
|
+
!['__proto__', 'prototype', 'constructor'].includes(name));
|
|
31
|
+
}
|
|
32
|
+
/** stdio transport — JSON-RPC 2.0, newline-delimited ผ่าน child process stdin/stdout */
|
|
33
|
+
class StdioTransport {
|
|
27
34
|
proc;
|
|
28
35
|
buf = '';
|
|
29
36
|
nextId = 1;
|
|
@@ -34,6 +41,9 @@ class McpClient {
|
|
|
34
41
|
// minimal env เท่านั้น (PATH/HOME/locale) + cfg.env ที่ user ตั้งเอง — ไม่ส่ง secret
|
|
35
42
|
// (ANTHROPIC_API_KEY/TELEGRAM_BOT_TOKEN/ฯลฯ) ให้ทุก MCP server (supply chain = npx -y <pkg>)
|
|
36
43
|
env: { ...safeEnv(), ...cfg.env },
|
|
44
|
+
// Windows: `npx`/`npm`/JS bins เป็น .cmd shim → spawn ตรงๆ = ENOENT. shell=true ให้ผ่าน PATHEXT.
|
|
45
|
+
// (config นี้ user เป็นเจ้าของ/trust แล้ว — bare-name resolution เท่านั้น)
|
|
46
|
+
shell: process.platform === 'win32',
|
|
37
47
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
38
48
|
});
|
|
39
49
|
this.proc.stdout?.on('data', (d) => this.onData(d.toString()));
|
|
@@ -76,7 +86,7 @@ class McpClient {
|
|
|
76
86
|
}
|
|
77
87
|
}
|
|
78
88
|
}
|
|
79
|
-
request(method, params, timeoutMs =
|
|
89
|
+
request(method, params, timeoutMs = REQUEST_TIMEOUT) {
|
|
80
90
|
if (this.dead)
|
|
81
91
|
return Promise.reject(new Error('mcp: server ตายแล้ว'));
|
|
82
92
|
const id = this.nextId++;
|
|
@@ -101,20 +111,115 @@ class McpClient {
|
|
|
101
111
|
notify(method, params) {
|
|
102
112
|
this.proc.stdin?.write(`${JSON.stringify({ jsonrpc: '2.0', method, params })}\n`);
|
|
103
113
|
}
|
|
114
|
+
close() {
|
|
115
|
+
try {
|
|
116
|
+
this.proc.kill();
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
/* ตายแล้ว */
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** Streamable-HTTP transport — POST JSON-RPC ต่อ request, รับ application/json หรือ text/event-stream */
|
|
124
|
+
class HttpTransport {
|
|
125
|
+
url;
|
|
126
|
+
userHeaders;
|
|
127
|
+
nextId = 1;
|
|
128
|
+
sessionId;
|
|
129
|
+
constructor(url, userHeaders = {}) {
|
|
130
|
+
this.url = url;
|
|
131
|
+
this.userHeaders = userHeaders;
|
|
132
|
+
}
|
|
133
|
+
headers() {
|
|
134
|
+
return {
|
|
135
|
+
'content-type': 'application/json',
|
|
136
|
+
accept: 'application/json, text/event-stream',
|
|
137
|
+
...(this.sessionId ? { 'mcp-session-id': this.sessionId } : {}),
|
|
138
|
+
...this.userHeaders,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/** parse SSE body หา JSON-RPC response ที่ id ตรง (Streamable-HTTP คืน response ผ่าน event-stream ได้) */
|
|
142
|
+
parseSse(text, id) {
|
|
143
|
+
for (const block of text.split(/\n\n/)) {
|
|
144
|
+
const data = block
|
|
145
|
+
.split(/\n/)
|
|
146
|
+
.filter((l) => l.startsWith('data:'))
|
|
147
|
+
.map((l) => l.slice(5).trim())
|
|
148
|
+
.join('');
|
|
149
|
+
if (!data)
|
|
150
|
+
continue;
|
|
151
|
+
let msg;
|
|
152
|
+
try {
|
|
153
|
+
msg = JSON.parse(data);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
continue; // block นี้ไม่ใช่ JSON สมบูรณ์ → ข้ามไป block ถัดไป (ไม่ abort ทั้ง stream)
|
|
157
|
+
}
|
|
158
|
+
// MCP protocol error / return อยู่นอก try → ไม่ถูกกลบโดย catch ของ JSON.parse
|
|
159
|
+
if (msg.id === id) {
|
|
160
|
+
if (msg.error)
|
|
161
|
+
throw new Error(msg.error.message ?? 'mcp error');
|
|
162
|
+
return msg.result;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
throw new Error('mcp http: ไม่พบ response ใน event-stream');
|
|
166
|
+
}
|
|
167
|
+
async request(method, params, timeoutMs = REQUEST_TIMEOUT) {
|
|
168
|
+
const id = this.nextId++;
|
|
169
|
+
const res = await fetch(this.url, {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
headers: this.headers(),
|
|
172
|
+
body: JSON.stringify({ jsonrpc: '2.0', id, method, params }),
|
|
173
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
174
|
+
});
|
|
175
|
+
const sid = res.headers.get('mcp-session-id');
|
|
176
|
+
if (sid)
|
|
177
|
+
this.sessionId = sid;
|
|
178
|
+
if (!res.ok)
|
|
179
|
+
throw new Error(`mcp http ${res.status} ${res.statusText}`);
|
|
180
|
+
const ctype = res.headers.get('content-type') ?? '';
|
|
181
|
+
if (ctype.includes('text/event-stream'))
|
|
182
|
+
return this.parseSse(await res.text(), id);
|
|
183
|
+
const json = (await res.json());
|
|
184
|
+
if (json.error)
|
|
185
|
+
throw new Error(json.error.message ?? 'mcp error');
|
|
186
|
+
return json.result;
|
|
187
|
+
}
|
|
188
|
+
notify(method, params) {
|
|
189
|
+
void fetch(this.url, {
|
|
190
|
+
method: 'POST',
|
|
191
|
+
headers: this.headers(),
|
|
192
|
+
body: JSON.stringify({ jsonrpc: '2.0', method, params }),
|
|
193
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT),
|
|
194
|
+
}).catch(() => { });
|
|
195
|
+
}
|
|
196
|
+
close() {
|
|
197
|
+
if (!this.sessionId)
|
|
198
|
+
return;
|
|
199
|
+
// best-effort terminate session (spec: DELETE) — ไม่รอผล
|
|
200
|
+
void fetch(this.url, { method: 'DELETE', headers: this.headers() }).catch(() => { });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/** MCP client — เลือก transport จาก config (url = http, ไม่งั้น stdio) แล้ว handshake + เรียก tool */
|
|
204
|
+
class McpClient {
|
|
205
|
+
transport;
|
|
206
|
+
constructor(cfg) {
|
|
207
|
+
this.transport = cfg.url ? new HttpTransport(cfg.url, cfg.headers) : new StdioTransport(cfg);
|
|
208
|
+
}
|
|
104
209
|
async initialize() {
|
|
105
|
-
await this.request('initialize', {
|
|
210
|
+
await this.transport.request('initialize', {
|
|
106
211
|
protocolVersion: PROTOCOL_VERSION,
|
|
107
212
|
capabilities: {},
|
|
108
|
-
clientInfo: { name:
|
|
213
|
+
clientInfo: { name: BRAND.mcpClientName, version: VERSION },
|
|
109
214
|
});
|
|
110
|
-
this.notify('notifications/initialized');
|
|
215
|
+
this.transport.notify('notifications/initialized');
|
|
111
216
|
}
|
|
112
217
|
async listTools() {
|
|
113
|
-
const r = (await this.request('tools/list'));
|
|
218
|
+
const r = (await this.transport.request('tools/list'));
|
|
114
219
|
return r?.tools ?? [];
|
|
115
220
|
}
|
|
116
221
|
async callTool(name, args) {
|
|
117
|
-
const r = (await this.request('tools/call', { name, arguments: args ?? {} }));
|
|
222
|
+
const r = (await this.transport.request('tools/call', { name, arguments: args ?? {} }));
|
|
118
223
|
const text = (r?.content ?? [])
|
|
119
224
|
.filter((c) => c.type === 'text')
|
|
120
225
|
.map((c) => c.text ?? '')
|
|
@@ -122,25 +227,71 @@ class McpClient {
|
|
|
122
227
|
return r?.isError ? `MCP error: ${text}` : text || '(no output)';
|
|
123
228
|
}
|
|
124
229
|
close() {
|
|
125
|
-
|
|
126
|
-
this.proc.kill();
|
|
127
|
-
}
|
|
128
|
-
catch {
|
|
129
|
-
/* ตายแล้ว */
|
|
130
|
-
}
|
|
230
|
+
this.transport.close();
|
|
131
231
|
}
|
|
132
232
|
}
|
|
133
|
-
|
|
233
|
+
function stringRecord(value) {
|
|
234
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
235
|
+
return undefined;
|
|
236
|
+
const out = {};
|
|
237
|
+
for (const [k, v] of Object.entries(value)) {
|
|
238
|
+
if (typeof k === 'string' && typeof v === 'string')
|
|
239
|
+
out[k] = v;
|
|
240
|
+
}
|
|
241
|
+
return Object.keys(out).length ? out : undefined;
|
|
242
|
+
}
|
|
243
|
+
function sanitizeMcpServerConfig(raw) {
|
|
244
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
245
|
+
return null;
|
|
246
|
+
const r = raw;
|
|
247
|
+
const cfg = {};
|
|
248
|
+
if (typeof r.command === 'string' && r.command)
|
|
249
|
+
cfg.command = r.command;
|
|
250
|
+
if (Array.isArray(r.args) && r.args.every((a) => typeof a === 'string'))
|
|
251
|
+
cfg.args = r.args;
|
|
252
|
+
if (typeof r.url === 'string' && r.url)
|
|
253
|
+
cfg.url = r.url;
|
|
254
|
+
const env = stringRecord(r.env);
|
|
255
|
+
if (env)
|
|
256
|
+
cfg.env = env;
|
|
257
|
+
const headers = stringRecord(r.headers);
|
|
258
|
+
if (headers)
|
|
259
|
+
cfg.headers = headers;
|
|
260
|
+
return cfg.command || cfg.url ? cfg : null;
|
|
261
|
+
}
|
|
262
|
+
async function readMcpFile(path, merged) {
|
|
263
|
+
const cfg = JSON.parse(await readFile(path, 'utf8'));
|
|
264
|
+
if (!cfg.mcpServers || typeof cfg.mcpServers !== 'object')
|
|
265
|
+
return;
|
|
266
|
+
for (const [name, raw] of Object.entries(cfg.mcpServers)) {
|
|
267
|
+
if (!isValidMcpServerName(name))
|
|
268
|
+
continue;
|
|
269
|
+
const server = sanitizeMcpServerConfig(raw);
|
|
270
|
+
if (server)
|
|
271
|
+
merged[name] = server;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
export async function loadMcpConfig(onLog, cwd = process.cwd()) {
|
|
134
275
|
const merged = {};
|
|
135
|
-
|
|
276
|
+
try {
|
|
277
|
+
await readMcpFile(appHomePath('mcp.json'), merged);
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
/* ไม่มี global config = ข้าม */
|
|
281
|
+
}
|
|
282
|
+
const root = await projectRoot(cwd);
|
|
283
|
+
const projectPath = await projectConfigPathIfTrusted('mcp.json', root);
|
|
284
|
+
if (projectPath) {
|
|
136
285
|
try {
|
|
137
|
-
|
|
138
|
-
Object.assign(merged, cfg.mcpServers ?? {});
|
|
286
|
+
await readMcpFile(projectPath, merged);
|
|
139
287
|
}
|
|
140
|
-
catch {
|
|
141
|
-
|
|
288
|
+
catch (e) {
|
|
289
|
+
onLog?.(`project MCP config อ่านไม่ได้: ${e.message}`);
|
|
142
290
|
}
|
|
143
291
|
}
|
|
292
|
+
else if (await hasUntrustedProjectConfig('mcp.json', root)) {
|
|
293
|
+
onLog?.(`project MCP config ถูกข้าม (ยังไม่ trust): ${appProjectPath(root, 'mcp.json')}`);
|
|
294
|
+
}
|
|
144
295
|
return merged;
|
|
145
296
|
}
|
|
146
297
|
let cachePromise = null;
|
|
@@ -151,16 +302,20 @@ export function getMcpTools(onLog) {
|
|
|
151
302
|
return cachePromise;
|
|
152
303
|
}
|
|
153
304
|
async function buildMcpTools(onLog) {
|
|
154
|
-
const config = await loadMcpConfig();
|
|
305
|
+
const config = await loadMcpConfig(onLog);
|
|
155
306
|
if (!Object.keys(config).length)
|
|
156
307
|
return {};
|
|
157
308
|
const tools = {};
|
|
158
309
|
const clients = [];
|
|
159
310
|
activeClients = clients; // ref เดียวกัน → closeMcp kill client ที่ spawn ระหว่าง build ได้ด้วย
|
|
160
311
|
for (const [serverName, cfg] of Object.entries(config)) {
|
|
312
|
+
if (!cfg.url && !cfg.command) {
|
|
313
|
+
onLog?.(`MCP "${serverName}" ข้าม: ต้องมี "command" (stdio) หรือ "url" (remote)`);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
161
316
|
try {
|
|
162
317
|
const client = new McpClient(cfg);
|
|
163
|
-
clients.push(client); // push ทันที (
|
|
318
|
+
clients.push(client); // push ทันที (อาจ spawn แล้ว) ก่อน await → ไม่ leak ถ้า build ค้าง
|
|
164
319
|
await client.initialize();
|
|
165
320
|
const defs = await client.listTools();
|
|
166
321
|
for (const def of defs) {
|
|
@@ -175,7 +330,7 @@ async function buildMcpTools(onLog) {
|
|
|
175
330
|
execute: async (args) => client.callTool(def.name, args),
|
|
176
331
|
});
|
|
177
332
|
}
|
|
178
|
-
onLog?.(`MCP "${serverName}": ${defs.length} tools`);
|
|
333
|
+
onLog?.(`MCP "${serverName}" (${cfg.url ? 'http' : 'stdio'}): ${defs.length} tools`);
|
|
179
334
|
}
|
|
180
335
|
catch (e) {
|
|
181
336
|
onLog?.(`MCP "${serverName}" ต่อไม่ได้: ${e.message}`);
|