zames_pro 2.12.0 → 2.14.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/README.md +53 -0
- package/dist/agent-loop.js +43 -43
- package/dist/extraTools.js +352 -0
- package/dist/gitTools.js +13 -13
- package/dist/i18n.js +10 -0
- package/dist/index.js +55 -1
- package/dist/mcp.js +327 -0
- package/dist/system-prompt.js +10 -1
- package/dist/tools.js +100 -46
- package/dist/web.js +15 -15
- package/package.json +2 -1
package/dist/gitTools.js
CHANGED
|
@@ -18,16 +18,16 @@ export function runGit(cmd, cwd, timeout = 15_000) {
|
|
|
18
18
|
const errStr = (stderr || '').toString();
|
|
19
19
|
if (!err) {
|
|
20
20
|
const combined = (out + errStr).trim();
|
|
21
|
-
resolve(combined || '(
|
|
21
|
+
resolve(combined || '(command produced no output)');
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
24
|
const parts = [];
|
|
25
25
|
if (err.killed)
|
|
26
|
-
parts.push(`⏱
|
|
26
|
+
parts.push(`⏱ Timeout ${timeout}ms`);
|
|
27
27
|
else if (err.code !== undefined && err.code !== null)
|
|
28
28
|
parts.push(`Exit code: ${err.code}`);
|
|
29
29
|
else
|
|
30
|
-
parts.push(
|
|
30
|
+
parts.push(`Error: ${err.message}`);
|
|
31
31
|
if (out.trim())
|
|
32
32
|
parts.push(out.trim());
|
|
33
33
|
if (errStr.trim())
|
|
@@ -114,7 +114,7 @@ export function createGitTools(workdir) {
|
|
|
114
114
|
return [
|
|
115
115
|
{
|
|
116
116
|
name: 'GitStatus',
|
|
117
|
-
description: '
|
|
117
|
+
description: 'Show the git status of the working directory. Returns an error if this is not a git repository.',
|
|
118
118
|
parameters: {},
|
|
119
119
|
fn: async () => {
|
|
120
120
|
const err = await ensureRepo();
|
|
@@ -125,7 +125,7 @@ export function createGitTools(workdir) {
|
|
|
125
125
|
},
|
|
126
126
|
{
|
|
127
127
|
name: 'GitDiff',
|
|
128
|
-
description: '
|
|
128
|
+
description: 'Show git diff. By default unstaged changes. Can be limited with path and staged=true.',
|
|
129
129
|
parameters: { path: 'string?', staged: 'boolean?' },
|
|
130
130
|
fn: async ({ path: p, staged }) => {
|
|
131
131
|
const err = await ensureRepo();
|
|
@@ -141,7 +141,7 @@ export function createGitTools(workdir) {
|
|
|
141
141
|
},
|
|
142
142
|
{
|
|
143
143
|
name: 'GitLog',
|
|
144
|
-
description: '
|
|
144
|
+
description: 'Show the last N commits (default 10).',
|
|
145
145
|
parameters: { count: 'number?' },
|
|
146
146
|
fn: async ({ count }) => {
|
|
147
147
|
const err = await ensureRepo();
|
|
@@ -152,7 +152,7 @@ export function createGitTools(workdir) {
|
|
|
152
152
|
},
|
|
153
153
|
{
|
|
154
154
|
name: 'GitAdd',
|
|
155
|
-
description: '
|
|
155
|
+
description: 'Stage files. If paths is not given — git add -A (everything).',
|
|
156
156
|
parameters: { paths: 'string?' },
|
|
157
157
|
fn: async ({ paths }) => {
|
|
158
158
|
const err = await ensureRepo();
|
|
@@ -165,7 +165,7 @@ export function createGitTools(workdir) {
|
|
|
165
165
|
},
|
|
166
166
|
{
|
|
167
167
|
name: 'GitCommit',
|
|
168
|
-
description: '
|
|
168
|
+
description: 'Commit staged changes (if nothing is staged — git add -A first). Does not push.',
|
|
169
169
|
parameters: { message: 'string' },
|
|
170
170
|
fn: async ({ message }) => {
|
|
171
171
|
const err = await ensureRepo();
|
|
@@ -173,14 +173,14 @@ export function createGitTools(workdir) {
|
|
|
173
173
|
return err;
|
|
174
174
|
const msg = String(message || '');
|
|
175
175
|
if (!msg.trim()) {
|
|
176
|
-
return '
|
|
176
|
+
return 'Error: message is empty.';
|
|
177
177
|
}
|
|
178
178
|
// We stage everything only if the index is empty (as the description promises).
|
|
179
179
|
const staged = await runGit('git diff --cached --name-only', workdir, 10_000);
|
|
180
180
|
const hasStaged = !/^\s*/.test(staged) &&
|
|
181
|
-
staged.trim() !== '(
|
|
181
|
+
staged.trim() !== '(command produced no output)';
|
|
182
182
|
const addResult = hasStaged
|
|
183
|
-
? '(
|
|
183
|
+
? '(index is not empty — add -A skipped)'
|
|
184
184
|
: await runGit('git add -A', workdir, 20_000);
|
|
185
185
|
const fs = await import('fs/promises');
|
|
186
186
|
const path = await import('path');
|
|
@@ -198,7 +198,7 @@ export function createGitTools(workdir) {
|
|
|
198
198
|
},
|
|
199
199
|
{
|
|
200
200
|
name: 'GitPush',
|
|
201
|
-
description: '
|
|
201
|
+
description: 'Push a branch to origin. Requires configured credentials (SSH or Windows Credential Manager).',
|
|
202
202
|
parameters: { branch: 'string?', setUpstream: 'boolean?' },
|
|
203
203
|
fn: async ({ branch, setUpstream }) => {
|
|
204
204
|
const err = await ensureRepo();
|
|
@@ -208,7 +208,7 @@ export function createGitTools(workdir) {
|
|
|
208
208
|
? String(branch).trim()
|
|
209
209
|
: (await runGit('git branch --show-current', workdir, 5000)).trim();
|
|
210
210
|
if (!b)
|
|
211
|
-
return '
|
|
211
|
+
return 'Error: could not determine the branch to push.';
|
|
212
212
|
const flag = setUpstream ? '-u ' : '';
|
|
213
213
|
const target = quoteArgs(b);
|
|
214
214
|
return runGit(`git push ${flag}origin ${target}`, workdir, 120_000);
|
package/dist/i18n.js
CHANGED
|
@@ -246,6 +246,16 @@ const CATALOG = {
|
|
|
246
246
|
'cd.failed': { ru: 'Не удалось перейти: {v}', en: 'Failed to change directory: {v}' },
|
|
247
247
|
'calibrate': { ru: '\n🔧 Режим калибровки селекторов\n', en: '\n🔧 Selector calibration mode\n' },
|
|
248
248
|
'reload.auto_partial': { ru: '⚠ авто-reload: часть модулей не загрузилась, работаю на прежней версии:', en: '⚠ auto-reload: some modules failed, using previous version:' },
|
|
249
|
+
// ---------- MCP ----------
|
|
250
|
+
'help.cmd.mcp': { ru: '/mcp показать MCP-серверы и их инструменты', en: '/mcp show MCP servers and their tools' },
|
|
251
|
+
'mcp.loaded': { ru: 'MCP: подключено инструментов {n} ({servers})', en: 'MCP: {n} tool(s) from {servers}' },
|
|
252
|
+
'mcp.server_error': { ru: 'MCP-сервер {name} недоступен: {error}', en: 'MCP server {name} unavailable: {error}' },
|
|
253
|
+
'mcp.load_failed': { ru: 'Не удалось загрузить MCP: {v}', en: 'Failed to load MCP: {v}' },
|
|
254
|
+
'mcp.none': { ru: 'MCP-серверы не настроены.', en: 'No MCP servers configured.' },
|
|
255
|
+
'mcp.hint': { ru: 'Добавь ~/.zames/mcp.json или <project>/.zames/mcp.json.', en: 'Add ~/.zames/mcp.json or <project>/.zames/mcp.json.' },
|
|
256
|
+
'mcp.title': { ru: 'MCP-серверы (инструментов: {n}):', en: 'MCP servers ({n} tools):' },
|
|
257
|
+
'mcp.status_error': { ru: '(ошибка: {v})', en: '(error: {v})' },
|
|
258
|
+
'status.mcp': { ru: 'MCP-инструменты: {v}', en: 'MCP tools: {v}' },
|
|
249
259
|
// ---------- config menu ----------
|
|
250
260
|
'cfg.group.ui': { ru: 'Интерфейс', en: 'Interface' },
|
|
251
261
|
'cfg.group.agent': { ru: 'Агент', en: 'Agent' },
|
package/dist/index.js
CHANGED
|
@@ -91,7 +91,9 @@ function loadLastChat(workdir = '') {
|
|
|
91
91
|
// so we pick the extension ourselves — from the actual file of the current module.
|
|
92
92
|
const SRC_EXT = /[.]ts$/.test(new URL(import.meta.url).pathname) ? '.ts' : '.js';
|
|
93
93
|
const RELOADABLE = [
|
|
94
|
+
'mcp',
|
|
94
95
|
'tools',
|
|
96
|
+
'extraTools',
|
|
95
97
|
'agent-loop',
|
|
96
98
|
'system-prompt',
|
|
97
99
|
'gitTools',
|
|
@@ -114,6 +116,7 @@ const mod = {
|
|
|
114
116
|
selfList,
|
|
115
117
|
closeWeb,
|
|
116
118
|
createSpinner,
|
|
119
|
+
createMcpPool: null,
|
|
117
120
|
};
|
|
118
121
|
async function reloadModules() {
|
|
119
122
|
const stamp = Date.now();
|
|
@@ -150,6 +153,8 @@ async function reloadModules() {
|
|
|
150
153
|
mod.closeWeb = pick('web', 'closeWeb');
|
|
151
154
|
if (pick('spinner', 'createSpinner'))
|
|
152
155
|
mod.createSpinner = pick('spinner', 'createSpinner');
|
|
156
|
+
if (pick('mcp', 'createMcpPool'))
|
|
157
|
+
mod.createMcpPool = pick('mcp', 'createMcpPool');
|
|
153
158
|
return { count: loaded.size, errors };
|
|
154
159
|
}
|
|
155
160
|
// Initial load so that mod.buildSystemPrompt and the rest are populated.
|
|
@@ -265,6 +270,7 @@ const SLASH_COMMANDS = [
|
|
|
265
270
|
{ name: '/skills', key: 'help.cmd.skills' },
|
|
266
271
|
{ name: '/memory', key: 'help.cmd.memory' },
|
|
267
272
|
{ name: '/init', key: 'help.cmd.init' },
|
|
273
|
+
{ name: '/mcp', key: 'help.cmd.mcp' },
|
|
268
274
|
{ name: '/debug-dom', key: 'help.cmd.debug_dom' },
|
|
269
275
|
{ name: '/self-review', key: 'help.self.review' },
|
|
270
276
|
{ name: '/self-fix', key: 'help.self.fix' },
|
|
@@ -820,7 +826,7 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
820
826
|
// case no assistant message was shown, and the operator saw the run
|
|
821
827
|
// just "stop" after a tool call with no explanation. Surface it.
|
|
822
828
|
if (outcome &&
|
|
823
|
-
(outcome.startsWith('
|
|
829
|
+
(outcome.startsWith('Iteration limit reached') ||
|
|
824
830
|
outcome.startsWith('ask() watchdog'))) {
|
|
825
831
|
ui.warning(outcome);
|
|
826
832
|
transcript?.log('agent_no_answer', { outcome });
|
|
@@ -901,6 +907,23 @@ async function main() {
|
|
|
901
907
|
console.log(theme.system(t('msg.transcript', { v: transcript.file })));
|
|
902
908
|
}
|
|
903
909
|
const undo = new UndoStore(config.undo);
|
|
910
|
+
// MCP servers: optional external tool providers (e.g. @playwright/mcp).
|
|
911
|
+
let mcpPool = null;
|
|
912
|
+
try {
|
|
913
|
+
mcpPool = await mod.createMcpPool({ workdir: currentWorkdir });
|
|
914
|
+
const st = mcpPool.status();
|
|
915
|
+
if (st.toolCount > 0) {
|
|
916
|
+
const names = st.servers.filter((x) => !x.error).map((x) => x.name).join(', ');
|
|
917
|
+
console.log(theme.system(t('mcp.loaded', { n: String(st.toolCount), servers: names })));
|
|
918
|
+
}
|
|
919
|
+
for (const srv of st.servers) {
|
|
920
|
+
if (srv.error)
|
|
921
|
+
console.error(theme.warn(t('mcp.server_error', { name: srv.name, error: srv.error })));
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
catch (e) {
|
|
925
|
+
console.error(theme.warn(t('mcp.load_failed', { v: e.message })));
|
|
926
|
+
}
|
|
904
927
|
const browser = new DeepSeekBrowser({
|
|
905
928
|
headless,
|
|
906
929
|
debug,
|
|
@@ -926,6 +949,8 @@ async function main() {
|
|
|
926
949
|
// One-shot mode
|
|
927
950
|
if (task) {
|
|
928
951
|
const tools = mod.createTools(currentWorkdir, { undo });
|
|
952
|
+
if (mcpPool)
|
|
953
|
+
tools.push(...mcpPool.tools);
|
|
929
954
|
let freshChat = true;
|
|
930
955
|
let sendSystemPrompt = true;
|
|
931
956
|
// By default we start a new chat. To continue a previous session —
|
|
@@ -962,6 +987,8 @@ async function main() {
|
|
|
962
987
|
});
|
|
963
988
|
await browser.close();
|
|
964
989
|
await mod.closeWeb().catch(() => { });
|
|
990
|
+
if (mcpPool)
|
|
991
|
+
await mcpPool.close().catch(() => { });
|
|
965
992
|
transcript.close();
|
|
966
993
|
return;
|
|
967
994
|
}
|
|
@@ -990,6 +1017,8 @@ async function main() {
|
|
|
990
1017
|
// Close the lazy headless browser from web.js, otherwise it would stay
|
|
991
1018
|
// as a separate process after the agent exits.
|
|
992
1019
|
await mod.closeWeb().catch(() => { });
|
|
1020
|
+
if (mcpPool)
|
|
1021
|
+
await mcpPool.close().catch(() => { });
|
|
993
1022
|
transcript.close();
|
|
994
1023
|
console.log(theme.system(String.fromCharCode(10) + t('msg.bye')));
|
|
995
1024
|
process.exit(0);
|
|
@@ -1752,6 +1781,24 @@ async function main() {
|
|
|
1752
1781
|
console.log(theme.system(currentWorkdir));
|
|
1753
1782
|
continue;
|
|
1754
1783
|
}
|
|
1784
|
+
if (lower === '/mcp') {
|
|
1785
|
+
if (!mcpPool) {
|
|
1786
|
+
console.log(theme.dim(t('mcp.none')));
|
|
1787
|
+
console.log(theme.dim(t('mcp.hint')));
|
|
1788
|
+
continue;
|
|
1789
|
+
}
|
|
1790
|
+
const st = mcpPool.status();
|
|
1791
|
+
console.log(theme.system(t('mcp.title', { n: String(st.toolCount) })));
|
|
1792
|
+
for (const srv of st.servers) {
|
|
1793
|
+
if (srv.error) {
|
|
1794
|
+
console.log(' ' + theme.user(srv.name) + ' ' + theme.dim(t('mcp.status_error', { v: srv.error })));
|
|
1795
|
+
}
|
|
1796
|
+
else {
|
|
1797
|
+
console.log(' ' + theme.user(srv.name) + ' ' + theme.dim('(' + String(srv.tools.length) + ')'));
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
continue;
|
|
1801
|
+
}
|
|
1755
1802
|
if (lower === '/skills') {
|
|
1756
1803
|
const { loadSkills } = await import('./context.js');
|
|
1757
1804
|
const skills = await loadSkills(currentWorkdir);
|
|
@@ -1926,6 +1973,9 @@ async function main() {
|
|
|
1926
1973
|
v: transcript.file || t('common.off'),
|
|
1927
1974
|
})));
|
|
1928
1975
|
console.log(theme.system(t('status.locale', { v: localeDisplayName(currentLocale) })));
|
|
1976
|
+
console.log(theme.system(t('status.mcp', {
|
|
1977
|
+
v: mcpPool ? String(mcpPool.status().toolCount) : t('common.none'),
|
|
1978
|
+
})));
|
|
1929
1979
|
continue;
|
|
1930
1980
|
}
|
|
1931
1981
|
if (lower === '/config' || lower.startsWith('/config ')) {
|
|
@@ -2032,6 +2082,8 @@ async function main() {
|
|
|
2032
2082
|
await autoReload();
|
|
2033
2083
|
transcript.log('user_task', { task: taskText, workdir: currentWorkdir });
|
|
2034
2084
|
const tools = mod.createTools(currentWorkdir, { undo });
|
|
2085
|
+
if (mcpPool)
|
|
2086
|
+
tools.push(...mcpPool.tools);
|
|
2035
2087
|
if (editor)
|
|
2036
2088
|
editor.busy = true;
|
|
2037
2089
|
try {
|
|
@@ -2067,6 +2119,8 @@ async function main() {
|
|
|
2067
2119
|
editor.dispose();
|
|
2068
2120
|
await browser.close().catch(() => { });
|
|
2069
2121
|
await mod.closeWeb().catch(() => { });
|
|
2122
|
+
if (mcpPool)
|
|
2123
|
+
await mcpPool.close().catch(() => { });
|
|
2070
2124
|
transcript.close();
|
|
2071
2125
|
}
|
|
2072
2126
|
main().catch((e) => {
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/** Names of the servers found in a config, before any connection. */
|
|
2
|
+
export async function listConfiguredServers(workdir) {
|
|
3
|
+
const files = await discoverConfigFiles(workdir);
|
|
4
|
+
const config = await loadConfigFiles(files);
|
|
5
|
+
const servers = config.mcpServers || {};
|
|
6
|
+
return Object.keys(servers).filter((n) => !servers[n].disabled);
|
|
7
|
+
}
|
|
8
|
+
// @playwright/mcp defaults its --user-data-dir to the SAME profile zames uses
|
|
9
|
+
// (~/.zames/profile). Two chromium instances on one profile break each other:
|
|
10
|
+
// the chat page shows "Something went wrong when opening your profile" and
|
|
11
|
+
// the session is lost. So when a stdio server looks like @playwright/mcp we
|
|
12
|
+
// auto-add --isolated (in-memory profile) unless the user already chose a
|
|
13
|
+
// profile (--isolated / --user-data-dir / --extension / --cdp-endpoint).
|
|
14
|
+
export function hardenPlaywrightArgs(command, args) {
|
|
15
|
+
const hay = (String(command || '') + ' ' + args.join(' ')).toLowerCase();
|
|
16
|
+
if (!hay.includes('playwright') || !hay.includes('mcp'))
|
|
17
|
+
return args;
|
|
18
|
+
if (args.some((a) => a === '--isolated' || a.startsWith('--user-data-dir') || a === '--extension' || a.startsWith('--cdp-endpoint'))) {
|
|
19
|
+
return args;
|
|
20
|
+
}
|
|
21
|
+
return [...args, '--isolated'];
|
|
22
|
+
}
|
|
23
|
+
/** Parse one config object into a validated map (best-effort). */
|
|
24
|
+
export function normalizeServerConfig(raw) {
|
|
25
|
+
if (!raw || typeof raw !== 'object')
|
|
26
|
+
return null;
|
|
27
|
+
const r = raw;
|
|
28
|
+
const out = {};
|
|
29
|
+
if (typeof r.command === 'string')
|
|
30
|
+
out.command = r.command;
|
|
31
|
+
if (Array.isArray(r.args))
|
|
32
|
+
out.args = r.args.map((a) => String(a));
|
|
33
|
+
if (r.env && typeof r.env === 'object') {
|
|
34
|
+
out.env = {};
|
|
35
|
+
for (const [k, v] of Object.entries(r.env)) {
|
|
36
|
+
out.env[k] = String(v);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (typeof r.cwd === 'string')
|
|
40
|
+
out.cwd = r.cwd;
|
|
41
|
+
if (typeof r.url === 'string')
|
|
42
|
+
out.url = r.url;
|
|
43
|
+
if (r.transport === 'sse' || r.transport === 'streamable-http') {
|
|
44
|
+
out.transport = r.transport;
|
|
45
|
+
}
|
|
46
|
+
if (r.headers && typeof r.headers === 'object') {
|
|
47
|
+
out.headers = {};
|
|
48
|
+
for (const [k, v] of Object.entries(r.headers)) {
|
|
49
|
+
out.headers[k] = String(v);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (r.disabled === true)
|
|
53
|
+
out.disabled = true;
|
|
54
|
+
if (!out.command && !out.url)
|
|
55
|
+
return null;
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
/** Candidate config paths, ordered from global to project-local. */
|
|
59
|
+
export async function discoverConfigFiles(workdir) {
|
|
60
|
+
const path = await import('path');
|
|
61
|
+
const os = await import('os');
|
|
62
|
+
const fsMod = await import('fs/promises');
|
|
63
|
+
const candidates = [
|
|
64
|
+
path.join(os.homedir(), '.zames', 'mcp.json'),
|
|
65
|
+
path.join(workdir, '.zames', 'mcp.json'),
|
|
66
|
+
path.join(workdir, '.mcp.json'),
|
|
67
|
+
];
|
|
68
|
+
const out = [];
|
|
69
|
+
for (const c of candidates) {
|
|
70
|
+
if (await fsMod.stat(c).catch(() => null))
|
|
71
|
+
out.push(c);
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
/** Read and merge every config file. Later files override earlier ones. */
|
|
76
|
+
export async function loadConfigFiles(files) {
|
|
77
|
+
const merged = {};
|
|
78
|
+
const fsMod = await import('fs/promises');
|
|
79
|
+
for (const f of files) {
|
|
80
|
+
try {
|
|
81
|
+
const raw = await fsMod.readFile(f, 'utf-8');
|
|
82
|
+
const json = JSON.parse(raw);
|
|
83
|
+
const servers = json.mcpServers;
|
|
84
|
+
if (!servers || typeof servers !== 'object')
|
|
85
|
+
continue;
|
|
86
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
87
|
+
const normalized = normalizeServerConfig(cfg);
|
|
88
|
+
if (normalized)
|
|
89
|
+
merged[name] = normalized;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Best-effort: a broken MCP config must not break the agent.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { mcpServers: merged };
|
|
97
|
+
}
|
|
98
|
+
/* ---------- json-schema -> agent parameters ---------- */
|
|
99
|
+
// MCP tools describe their arguments with a JSON Schema. zames own ToolDef
|
|
100
|
+
// uses a flat map (name -> 'type?'), which is what the system-prompt shows
|
|
101
|
+
// to the model. We convert the schema into that flat form so MCP tools look
|
|
102
|
+
// like built-in ones and the model already knows the convention.
|
|
103
|
+
//
|
|
104
|
+
// We also keep the full schema in the tool description, so the model sees
|
|
105
|
+
// descriptions/enums/required fields that the flat map cannot convey.
|
|
106
|
+
export function jsonSchemaToParams(schema) {
|
|
107
|
+
const out = {};
|
|
108
|
+
if (!schema || typeof schema !== 'object')
|
|
109
|
+
return out;
|
|
110
|
+
const s = schema;
|
|
111
|
+
if (!s.properties || typeof s.properties !== 'object')
|
|
112
|
+
return out;
|
|
113
|
+
const required = new Set(Array.isArray(s.required) ? s.required : []);
|
|
114
|
+
for (const [key, raw] of Object.entries(s.properties)) {
|
|
115
|
+
const prop = (raw || {});
|
|
116
|
+
let type = typeof prop.type === 'string' ? prop.type : '';
|
|
117
|
+
if (Array.isArray(prop.type) || prop.enum)
|
|
118
|
+
type = 'value';
|
|
119
|
+
if (!type)
|
|
120
|
+
type = 'value';
|
|
121
|
+
out[key] = type + (required.has(key) ? '' : '?');
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
// Append the raw JSON Schema to the description so the model sees field
|
|
126
|
+
// descriptions and enums. The flat map alone cannot convey them, and MCP
|
|
127
|
+
// tools (browser_snapshot, browser_click, ...) rely on them heavily.
|
|
128
|
+
export function describeParamsExternally(description, schema) {
|
|
129
|
+
const type = schema?.type;
|
|
130
|
+
if (!schema || type !== 'object')
|
|
131
|
+
return description;
|
|
132
|
+
try {
|
|
133
|
+
const json = JSON.stringify(schema);
|
|
134
|
+
if (json.length > 4000)
|
|
135
|
+
return description;
|
|
136
|
+
return description + '\nJSON Schema: ' + json;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return description;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/* ---------- result rendering ---------- */
|
|
143
|
+
// MCP tool results are a list of content blocks (text, image, resource, ...)
|
|
144
|
+
// and an isError flag. The agent needs a single string for the tool result
|
|
145
|
+
// message. Images are summarized (the model cannot see them through
|
|
146
|
+
// DeepSeek web anyway), text is joined.
|
|
147
|
+
export function renderMcpResult(result) {
|
|
148
|
+
const r = (result || {});
|
|
149
|
+
if (r.toolResult !== undefined && !r.content) {
|
|
150
|
+
return formatResultString(r.toolResult, r.isError);
|
|
151
|
+
}
|
|
152
|
+
const parts = [];
|
|
153
|
+
for (const block of r.content || []) {
|
|
154
|
+
if (!block || typeof block !== 'object') {
|
|
155
|
+
parts.push(String(block ?? ''));
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const b = block;
|
|
159
|
+
const type = String(b.type ?? '');
|
|
160
|
+
if (type === 'text') {
|
|
161
|
+
parts.push(String(b.text ?? ''));
|
|
162
|
+
}
|
|
163
|
+
else if (type === 'image') {
|
|
164
|
+
parts.push('[image ' + String(b.mimeType ?? 'unknown') + ', written to disk by the MCP server]');
|
|
165
|
+
}
|
|
166
|
+
else if (type === 'resource') {
|
|
167
|
+
const res = (b.resource || {});
|
|
168
|
+
if (typeof res.text === 'string') {
|
|
169
|
+
parts.push(String(res.uri ?? '') + String.fromCharCode(10) + res.text);
|
|
170
|
+
}
|
|
171
|
+
else if (typeof res.blob === 'string') {
|
|
172
|
+
parts.push('[binary resource ' + String(res.uri ?? '') + ', ' + String(res.mimeType ?? '') + ']');
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
parts.push(String(res.uri ?? ''));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else if (type === 'resource_link') {
|
|
179
|
+
parts.push(String(b.uri ?? b.name ?? ''));
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
try {
|
|
183
|
+
parts.push(JSON.stringify(b));
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
parts.push(String(b));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const text = parts.filter((p) => p !== '').join(String.fromCharCode(10) + String.fromCharCode(10)).trim();
|
|
191
|
+
return formatResultString(text || '(MCP tool returned no content)', r.isError);
|
|
192
|
+
}
|
|
193
|
+
function formatResultString(result, isError) {
|
|
194
|
+
const text = typeof result === 'string' ? result : safeJson(result);
|
|
195
|
+
return isError ? 'MCP error: ' + text : text;
|
|
196
|
+
}
|
|
197
|
+
function safeJson(v) {
|
|
198
|
+
try {
|
|
199
|
+
const s = JSON.stringify(v);
|
|
200
|
+
return s === undefined ? String(v) : s;
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return String(v);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Namespacing: MCP tool names are prefixed with the server name, so the
|
|
207
|
+
// system-prompt (and the transcript) show where a tool comes from and two
|
|
208
|
+
// servers cannot collide. The prefix uses __ (double underscore) because
|
|
209
|
+
// the model must be able to reproduce it exactly.
|
|
210
|
+
export function qualifyToolName(server, tool) {
|
|
211
|
+
return server + '__' + tool.replace(/[^A-Za-z0-9_]/g, '_');
|
|
212
|
+
}
|
|
213
|
+
// Connect to ONE server and return a ready connection (or an error).
|
|
214
|
+
async function connectServer(name, cfg, debug) {
|
|
215
|
+
try {
|
|
216
|
+
const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');
|
|
217
|
+
const client = new Client({ name: 'zames', version: '1.0.0' }, { capabilities: {} });
|
|
218
|
+
let transport;
|
|
219
|
+
if (cfg.url) {
|
|
220
|
+
if (cfg.transport === 'sse') {
|
|
221
|
+
const { SSEClientTransport } = await import('@modelcontextprotocol/sdk/client/sse.js');
|
|
222
|
+
transport = new SSEClientTransport(new URL(cfg.url), {
|
|
223
|
+
requestInit: cfg.headers ? { headers: cfg.headers } : undefined,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
const { StreamableHTTPClientTransport } = await import('@modelcontextprotocol/sdk/client/streamableHttp.js');
|
|
228
|
+
transport = new StreamableHTTPClientTransport(new URL(cfg.url), {
|
|
229
|
+
requestInit: cfg.headers ? { headers: cfg.headers } : undefined,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js');
|
|
235
|
+
transport = new StdioClientTransport({
|
|
236
|
+
command: String(cfg.command),
|
|
237
|
+
args: hardenPlaywrightArgs(String(cfg.command), cfg.args || []),
|
|
238
|
+
env: cfg.env,
|
|
239
|
+
cwd: cfg.cwd,
|
|
240
|
+
stderr: 'pipe',
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
await client.connect(transport);
|
|
244
|
+
const listed = await client.listTools();
|
|
245
|
+
const tools = (listed.tools || []).map((t) => {
|
|
246
|
+
const tool = t;
|
|
247
|
+
return {
|
|
248
|
+
name: String(tool.name),
|
|
249
|
+
description: String(tool.description || ''),
|
|
250
|
+
schema: tool.inputSchema,
|
|
251
|
+
};
|
|
252
|
+
});
|
|
253
|
+
debug?.(`MCP: connected to ${name} (${tools.length} tools)`);
|
|
254
|
+
return { name, client, tools };
|
|
255
|
+
}
|
|
256
|
+
catch (e) {
|
|
257
|
+
const msg = e.message || String(e);
|
|
258
|
+
debug?.(`MCP: failed to connect to ${name}: ${msg}`);
|
|
259
|
+
return { error: msg };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Build a pool from the MCP configs found for a workdir.
|
|
264
|
+
* Never throws: a broken server is reported in the status and skipped.
|
|
265
|
+
*/
|
|
266
|
+
export async function createMcpPool(opts) {
|
|
267
|
+
const files = await discoverConfigFiles(opts.workdir);
|
|
268
|
+
const config = await loadConfigFiles(files);
|
|
269
|
+
const servers = config.mcpServers || {};
|
|
270
|
+
const names = Object.keys(servers).filter((n) => !servers[n].disabled);
|
|
271
|
+
const infos = [];
|
|
272
|
+
const connections = [];
|
|
273
|
+
const tools = [];
|
|
274
|
+
for (const name of names) {
|
|
275
|
+
const cfg = servers[name];
|
|
276
|
+
const res = await connectServer(name, cfg, opts.debug);
|
|
277
|
+
if ('error' in res) {
|
|
278
|
+
infos.push({ name, tools: [], error: res.error });
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
connections.push(res);
|
|
282
|
+
infos.push({ name, tools: res.tools.map((t) => t.name) });
|
|
283
|
+
for (const tool of res.tools) {
|
|
284
|
+
const qualified = qualifyToolName(name, tool.name);
|
|
285
|
+
const params = jsonSchemaToParams(tool.schema);
|
|
286
|
+
const description = describeParamsExternally((tool.description || tool.name) + ` (MCP server: ${name})`, tool.schema);
|
|
287
|
+
tools.push({
|
|
288
|
+
name: qualified,
|
|
289
|
+
description,
|
|
290
|
+
parameters: params,
|
|
291
|
+
fn: async (args) => {
|
|
292
|
+
try {
|
|
293
|
+
const result = await res.client.callTool({
|
|
294
|
+
name: tool.name,
|
|
295
|
+
arguments: args,
|
|
296
|
+
});
|
|
297
|
+
return renderMcpResult(result);
|
|
298
|
+
}
|
|
299
|
+
catch (e) {
|
|
300
|
+
return `MCP error (${name}/${tool.name}): ${e.message}`;
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
let closed = false;
|
|
307
|
+
return {
|
|
308
|
+
tools,
|
|
309
|
+
status: () => ({
|
|
310
|
+
servers: infos,
|
|
311
|
+
toolCount: tools.length,
|
|
312
|
+
}),
|
|
313
|
+
close: async () => {
|
|
314
|
+
if (closed)
|
|
315
|
+
return;
|
|
316
|
+
closed = true;
|
|
317
|
+
for (const c of connections) {
|
|
318
|
+
try {
|
|
319
|
+
await c.client.close();
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
// ignore
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
}
|
package/dist/system-prompt.js
CHANGED
|
@@ -50,7 +50,7 @@ export function renderContextSection(context) {
|
|
|
50
50
|
export function buildSystemPrompt({ workdir, tools, gitContext = null, locale = 'ru', attachments = [], context = null, }) {
|
|
51
51
|
const t = translate(locale);
|
|
52
52
|
const toolDescriptions = tools
|
|
53
|
-
.map((t2) => `### ${t2.name}\n${t2.description}\
|
|
53
|
+
.map((t2) => `### ${t2.name}\n${t2.description}\nParameters: ${JSON.stringify(t2.parameters)}`)
|
|
54
54
|
.join('\n\n');
|
|
55
55
|
const gitSection = gitContext
|
|
56
56
|
? `\n## Git context\n\n${gitContext}\n`
|
|
@@ -113,6 +113,15 @@ You have access to the following tools:
|
|
|
113
113
|
|
|
114
114
|
${toolDescriptions}
|
|
115
115
|
${gitSection}${attachSection}${contextSection}
|
|
116
|
+
## MCP tools
|
|
117
|
+
|
|
118
|
+
Some tools are provided by external MCP servers and their names look
|
|
119
|
+
like server__tool (for example playwright__browser_navigate). Treat
|
|
120
|
+
them exactly like the built-in tools. A tool name with the __ separator
|
|
121
|
+
and the (MCP server: ...) note in its description tells you which server
|
|
122
|
+
it belongs to. The JSON Schema in the description lists the exact fields
|
|
123
|
+
(including their descriptions and enums) - follow it.
|
|
124
|
+
|
|
116
125
|
## How to use tools
|
|
117
126
|
|
|
118
127
|
To call ONE tool, respond with ONLY a JSON object (no markdown fences, no extra text):
|