zames_pro 2.12.0 → 2.13.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 CHANGED
@@ -106,6 +106,42 @@ Skills and custom commands show up in the «/» completion list and in `/help`.
106
106
  /init [--force] analyze the project and create AGENTS.md
107
107
  ```
108
108
 
109
+ ## MCP (external tools)
110
+
111
+ zames can use tools from [MCP](https://modelcontextprotocol.io) servers.
112
+ The flagship example is @playwright/mcp: it gives the agent a real browser
113
+ (navigate, click, snapshot, type, ...) on top of the one zames already uses
114
+ for the DeepSeek chat.
115
+
116
+ Drop a config file (same shape as Claude Code / Cursor):
117
+
118
+ - `~/.zames/mcp.json` - global
119
+ - `<project>/.zames/mcp.json` - project-scoped (later files win)
120
+ - `<project>/.mcp.json` - the common MCP name
121
+
122
+ ```json
123
+ {
124
+ "mcpServers": {
125
+ "playwright": {
126
+ "command": "npx",
127
+ "args": ["-y", "@playwright/mcp@latest", "--headless", "--isolated"]
128
+ }
129
+ }
130
+ }
131
+ ```
132
+
133
+ IMPORTANT: keep the MCP browser isolated. @playwright/mcp defaults to the
134
+ SAME profile directory as zames (~/.zames/profile). If it is launched
135
+ without --isolated (or without its own --user-data-dir), the MCP browser
136
+ and the agent browser fight over one profile and the DeepSeek chat shows
137
+ "Something went wrong when opening your profile". Always pass --isolated
138
+ as in the example above.
139
+ Servers can also be remote ("url": "https://...", "transport": "sse").
140
+ Their tools show up in the agent as `server__tool` (e.g.
141
+ `playwright__browser_navigate`) and are listed with `/mcp` and in `/status`.
142
+ A server that fails to connect is skipped with a warning and never breaks the
143
+ agent.
144
+
109
145
  ## Configuration
110
146
 
111
147
  Global config: `~/.zames/config.json`
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,6 +91,7 @@ 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',
95
96
  'agent-loop',
96
97
  'system-prompt',
@@ -114,6 +115,7 @@ const mod = {
114
115
  selfList,
115
116
  closeWeb,
116
117
  createSpinner,
118
+ createMcpPool: null,
117
119
  };
118
120
  async function reloadModules() {
119
121
  const stamp = Date.now();
@@ -150,6 +152,8 @@ async function reloadModules() {
150
152
  mod.closeWeb = pick('web', 'closeWeb');
151
153
  if (pick('spinner', 'createSpinner'))
152
154
  mod.createSpinner = pick('spinner', 'createSpinner');
155
+ if (pick('mcp', 'createMcpPool'))
156
+ mod.createMcpPool = pick('mcp', 'createMcpPool');
153
157
  return { count: loaded.size, errors };
154
158
  }
155
159
  // Initial load so that mod.buildSystemPrompt and the rest are populated.
@@ -265,6 +269,7 @@ const SLASH_COMMANDS = [
265
269
  { name: '/skills', key: 'help.cmd.skills' },
266
270
  { name: '/memory', key: 'help.cmd.memory' },
267
271
  { name: '/init', key: 'help.cmd.init' },
272
+ { name: '/mcp', key: 'help.cmd.mcp' },
268
273
  { name: '/debug-dom', key: 'help.cmd.debug_dom' },
269
274
  { name: '/self-review', key: 'help.self.review' },
270
275
  { name: '/self-fix', key: 'help.self.fix' },
@@ -901,6 +906,23 @@ async function main() {
901
906
  console.log(theme.system(t('msg.transcript', { v: transcript.file })));
902
907
  }
903
908
  const undo = new UndoStore(config.undo);
909
+ // MCP servers: optional external tool providers (e.g. @playwright/mcp).
910
+ let mcpPool = null;
911
+ try {
912
+ mcpPool = await mod.createMcpPool({ workdir: currentWorkdir });
913
+ const st = mcpPool.status();
914
+ if (st.toolCount > 0) {
915
+ const names = st.servers.filter((x) => !x.error).map((x) => x.name).join(', ');
916
+ console.log(theme.system(t('mcp.loaded', { n: String(st.toolCount), servers: names })));
917
+ }
918
+ for (const srv of st.servers) {
919
+ if (srv.error)
920
+ console.error(theme.warn(t('mcp.server_error', { name: srv.name, error: srv.error })));
921
+ }
922
+ }
923
+ catch (e) {
924
+ console.error(theme.warn(t('mcp.load_failed', { v: e.message })));
925
+ }
904
926
  const browser = new DeepSeekBrowser({
905
927
  headless,
906
928
  debug,
@@ -926,6 +948,8 @@ async function main() {
926
948
  // One-shot mode
927
949
  if (task) {
928
950
  const tools = mod.createTools(currentWorkdir, { undo });
951
+ if (mcpPool)
952
+ tools.push(...mcpPool.tools);
929
953
  let freshChat = true;
930
954
  let sendSystemPrompt = true;
931
955
  // By default we start a new chat. To continue a previous session —
@@ -962,6 +986,8 @@ async function main() {
962
986
  });
963
987
  await browser.close();
964
988
  await mod.closeWeb().catch(() => { });
989
+ if (mcpPool)
990
+ await mcpPool.close().catch(() => { });
965
991
  transcript.close();
966
992
  return;
967
993
  }
@@ -990,6 +1016,8 @@ async function main() {
990
1016
  // Close the lazy headless browser from web.js, otherwise it would stay
991
1017
  // as a separate process after the agent exits.
992
1018
  await mod.closeWeb().catch(() => { });
1019
+ if (mcpPool)
1020
+ await mcpPool.close().catch(() => { });
993
1021
  transcript.close();
994
1022
  console.log(theme.system(String.fromCharCode(10) + t('msg.bye')));
995
1023
  process.exit(0);
@@ -1752,6 +1780,24 @@ async function main() {
1752
1780
  console.log(theme.system(currentWorkdir));
1753
1781
  continue;
1754
1782
  }
1783
+ if (lower === '/mcp') {
1784
+ if (!mcpPool) {
1785
+ console.log(theme.dim(t('mcp.none')));
1786
+ console.log(theme.dim(t('mcp.hint')));
1787
+ continue;
1788
+ }
1789
+ const st = mcpPool.status();
1790
+ console.log(theme.system(t('mcp.title', { n: String(st.toolCount) })));
1791
+ for (const srv of st.servers) {
1792
+ if (srv.error) {
1793
+ console.log(' ' + theme.user(srv.name) + ' ' + theme.dim(t('mcp.status_error', { v: srv.error })));
1794
+ }
1795
+ else {
1796
+ console.log(' ' + theme.user(srv.name) + ' ' + theme.dim('(' + String(srv.tools.length) + ')'));
1797
+ }
1798
+ }
1799
+ continue;
1800
+ }
1755
1801
  if (lower === '/skills') {
1756
1802
  const { loadSkills } = await import('./context.js');
1757
1803
  const skills = await loadSkills(currentWorkdir);
@@ -1926,6 +1972,9 @@ async function main() {
1926
1972
  v: transcript.file || t('common.off'),
1927
1973
  })));
1928
1974
  console.log(theme.system(t('status.locale', { v: localeDisplayName(currentLocale) })));
1975
+ console.log(theme.system(t('status.mcp', {
1976
+ v: mcpPool ? String(mcpPool.status().toolCount) : t('common.none'),
1977
+ })));
1929
1978
  continue;
1930
1979
  }
1931
1980
  if (lower === '/config' || lower.startsWith('/config ')) {
@@ -2032,6 +2081,8 @@ async function main() {
2032
2081
  await autoReload();
2033
2082
  transcript.log('user_task', { task: taskText, workdir: currentWorkdir });
2034
2083
  const tools = mod.createTools(currentWorkdir, { undo });
2084
+ if (mcpPool)
2085
+ tools.push(...mcpPool.tools);
2035
2086
  if (editor)
2036
2087
  editor.busy = true;
2037
2088
  try {
@@ -2067,6 +2118,8 @@ async function main() {
2067
2118
  editor.dispose();
2068
2119
  await browser.close().catch(() => { });
2069
2120
  await mod.closeWeb().catch(() => { });
2121
+ if (mcpPool)
2122
+ await mcpPool.close().catch(() => { });
2070
2123
  transcript.close();
2071
2124
  }
2072
2125
  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
+ }
@@ -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):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.12.0",
3
+ "version": "2.13.0",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {
@@ -36,6 +36,7 @@
36
36
  "node": ">=18"
37
37
  },
38
38
  "dependencies": {
39
+ "@modelcontextprotocol/sdk": "^1.30.1",
39
40
  "chalk": "^5.3.0",
40
41
  "cli-highlight": "^2.1.11",
41
42
  "markdansi": "^0.3.4",