@corbat-tech/coco 2.25.9 → 2.25.11

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/dist/cli/index.js CHANGED
@@ -121,6 +121,7 @@ function createDefaultConfigObject(projectName, language = "typescript") {
121
121
  temperature: 0,
122
122
  timeout: 12e4
123
123
  },
124
+ providerModels: {},
124
125
  quality: {
125
126
  minScore: 85,
126
127
  minCoverage: 80,
@@ -300,6 +301,7 @@ var init_schema = __esm({
300
301
  temperature: 0,
301
302
  timeout: 12e4
302
303
  }),
304
+ providerModels: z.record(z.string(), z.string()).optional(),
303
305
  quality: QualityConfigSchema.default({
304
306
  minScore: 85,
305
307
  minCoverage: 80,
@@ -597,6 +599,7 @@ function deepMergeConfig(base, override) {
597
599
  ...override,
598
600
  project: { ...base.project, ...override.project },
599
601
  provider: { ...base.provider, ...override.provider },
602
+ providerModels: { ...base.providerModels, ...override.providerModels },
600
603
  quality: { ...base.quality, ...override.quality },
601
604
  persistence: { ...base.persistence, ...override.persistence },
602
605
  // Merge optional sections only if present in either base or override
@@ -2565,7 +2568,11 @@ function getDefaultModel(provider) {
2565
2568
  function normalizeConfiguredModel(model) {
2566
2569
  if (typeof model !== "string") return void 0;
2567
2570
  const trimmed = model.trim();
2568
- return trimmed.length > 0 ? trimmed : void 0;
2571
+ if (trimmed.length === 0) return void 0;
2572
+ if (["default", "none", "null", "undefined"].includes(trimmed.toLowerCase())) {
2573
+ return void 0;
2574
+ }
2575
+ return trimmed;
2569
2576
  }
2570
2577
  function getDefaultProvider() {
2571
2578
  const envProvider = process.env["COCO_PROVIDER"]?.toLowerCase();
@@ -2588,6 +2595,10 @@ async function getLastUsedProvider() {
2588
2595
  async function getLastUsedModel(provider) {
2589
2596
  try {
2590
2597
  const config = await loadConfig(CONFIG_PATHS.config);
2598
+ const perProviderModel = normalizeConfiguredModel(config.providerModels?.[provider]);
2599
+ if (perProviderModel) {
2600
+ return perProviderModel;
2601
+ }
2591
2602
  if (config.provider.type === provider) {
2592
2603
  return normalizeConfiguredModel(config.provider.model);
2593
2604
  }
@@ -2627,6 +2638,11 @@ async function saveProviderPreference(provider, model) {
2627
2638
  }
2628
2639
  config.provider.type = provider;
2629
2640
  const normalizedModel = normalizeConfiguredModel(model);
2641
+ const persistedModel = normalizedModel ?? getDefaultModel(provider);
2642
+ config.providerModels = {
2643
+ ...config.providerModels,
2644
+ [provider]: persistedModel
2645
+ };
2630
2646
  if (normalizedModel) {
2631
2647
  config.provider.model = normalizedModel;
2632
2648
  } else {
@@ -2725,11 +2741,25 @@ async function migrateOldPreferences() {
2725
2741
  }
2726
2742
  if (oldPrefs.provider && VALID_PROVIDERS.includes(oldPrefs.provider)) {
2727
2743
  config.provider.type = oldPrefs.provider;
2744
+ config.providerModels = {
2745
+ ...config.providerModels
2746
+ };
2747
+ for (const [providerName, modelName] of Object.entries(oldPrefs.models ?? {})) {
2748
+ if (VALID_PROVIDERS.includes(providerName)) {
2749
+ const normalized = normalizeConfiguredModel(modelName);
2750
+ if (normalized) {
2751
+ config.providerModels[providerName] = normalized;
2752
+ }
2753
+ }
2754
+ }
2728
2755
  const modelForProvider = oldPrefs.provider ? oldPrefs.models?.[oldPrefs.provider] : void 0;
2729
- if (modelForProvider) {
2730
- config.provider.model = modelForProvider;
2756
+ const normalizedMigratedModel = normalizeConfiguredModel(modelForProvider);
2757
+ if (normalizedMigratedModel) {
2758
+ config.provider.model = normalizedMigratedModel;
2759
+ config.providerModels[oldPrefs.provider] = normalizedMigratedModel;
2731
2760
  } else {
2732
2761
  config.provider.model = getDefaultModel(oldPrefs.provider);
2762
+ config.providerModels[oldPrefs.provider] = config.provider.model;
2733
2763
  }
2734
2764
  await saveConfig(config, void 0, true);
2735
2765
  }
@@ -4472,7 +4502,9 @@ var init_openai = __esm({
4472
4502
  input,
4473
4503
  instructions: instructions ?? void 0,
4474
4504
  max_output_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
4475
- ...supportsTemp && { temperature: options?.temperature ?? this.config.temperature ?? 0 },
4505
+ ...supportsTemp && {
4506
+ temperature: options?.temperature ?? this.config.temperature ?? 0
4507
+ },
4476
4508
  store: false
4477
4509
  });
4478
4510
  return {
@@ -4507,7 +4539,9 @@ var init_openai = __esm({
4507
4539
  instructions: instructions ?? void 0,
4508
4540
  tools,
4509
4541
  max_output_tokens: options?.maxTokens ?? this.config.maxTokens ?? 8192,
4510
- ...supportsTemp && { temperature: options?.temperature ?? this.config.temperature ?? 0 },
4542
+ ...supportsTemp && {
4543
+ temperature: options?.temperature ?? this.config.temperature ?? 0
4544
+ },
4511
4545
  store: false
4512
4546
  });
4513
4547
  let content = "";
@@ -10235,6 +10269,7 @@ Rules:
10235
10269
  - If you need real-time data, CALL web_search. NEVER say "I don't have access to real-time data."
10236
10270
  - If an MCP tool exists for a service (tool names like \`mcp_<service>_...\`), prefer that MCP tool over generic \`web_fetch\` or \`http_fetch\`.
10237
10271
  - Use \`mcp_list_servers\` to inspect configured or connected MCP services. Do NOT use \`bash_exec\` to run \`coco mcp ...\` unless the user explicitly asked for that CLI command.
10272
+ - If the user asks you to use an MCP service and it is configured but disconnected, call \`mcp_connect_server\` first. This built-in flow may open a browser for OAuth. Do NOT ask the user for raw tokens when MCP OAuth is supported.
10238
10273
  - Before answering "I can't do that", check your full tool catalog below \u2014 you likely have a tool for it.
10239
10274
  - NEVER claim you cannot run a command because you lack credentials, access, or connectivity. bash_exec runs in the user's own shell environment and inherits their full PATH, kubeconfig, gcloud auth, AWS profiles, SSH keys, and every other tool installed on their machine. kubectl, gcloud, aws, docker, and any other CLI available to the user are available to you. ALWAYS attempt the command with bash_exec; report failure only if it actually returns a non-zero exit code.
10240
10275
 
@@ -21226,7 +21261,9 @@ function createProtectedMetadataCandidates(resourceUrl, headerUrl) {
21226
21261
  candidates.push(`${origin}/.well-known/oauth-protected-resource`);
21227
21262
  if (pathPart && pathPart !== "/") {
21228
21263
  candidates.push(`${origin}/.well-known/oauth-protected-resource${pathPart}`);
21229
- candidates.push(`${origin}/.well-known/oauth-protected-resource/${pathPart.replace(/^\//, "")}`);
21264
+ candidates.push(
21265
+ `${origin}/.well-known/oauth-protected-resource/${pathPart.replace(/^\//, "")}`
21266
+ );
21230
21267
  }
21231
21268
  return Array.from(new Set(candidates));
21232
21269
  }
@@ -22341,6 +22378,263 @@ var init_full_power_risk_mode = __esm({
22341
22378
  }
22342
22379
  });
22343
22380
 
22381
+ // src/mcp/tools.ts
22382
+ var tools_exports = {};
22383
+ __export(tools_exports, {
22384
+ createToolsFromMCPServer: () => createToolsFromMCPServer,
22385
+ extractOriginalToolName: () => extractOriginalToolName,
22386
+ getMCPToolInfo: () => getMCPToolInfo,
22387
+ jsonSchemaToZod: () => jsonSchemaToZod,
22388
+ registerMCPTools: () => registerMCPTools,
22389
+ wrapMCPTool: () => wrapMCPTool,
22390
+ wrapMCPTools: () => wrapMCPTools
22391
+ });
22392
+ function buildMcpToolDescription(serverName, tool) {
22393
+ const base = tool.description || `Tool '${tool.name}' exposed by MCP server '${serverName}'`;
22394
+ const lowerServer = serverName.toLowerCase();
22395
+ if (lowerServer.includes("atlassian") || lowerServer.includes("jira") || lowerServer.includes("confluence")) {
22396
+ return `${base}. Use this MCP tool for Atlassian/Jira/Confluence data. Prefer it over direct web_fetch or http_fetch for Atlassian content.`;
22397
+ }
22398
+ return `${base}. Exposed by MCP server '${serverName}'. Prefer this MCP tool over generic web/http fetch when accessing data from that connected service.`;
22399
+ }
22400
+ function jsonSchemaToZod(schema) {
22401
+ if (schema.enum && Array.isArray(schema.enum)) {
22402
+ const values = schema.enum;
22403
+ if (values.length > 0 && values.every((v) => typeof v === "string")) {
22404
+ return z.enum(values);
22405
+ }
22406
+ const literals = values.map((v) => z.literal(v));
22407
+ if (literals.length < 2) {
22408
+ return literals[0] ?? z.any();
22409
+ }
22410
+ return z.union(literals);
22411
+ }
22412
+ if (schema.const !== void 0) {
22413
+ return z.literal(schema.const);
22414
+ }
22415
+ if (schema.oneOf && Array.isArray(schema.oneOf)) {
22416
+ const schemas = schema.oneOf.map(jsonSchemaToZod);
22417
+ if (schemas.length >= 2) {
22418
+ return z.union(schemas);
22419
+ }
22420
+ return schemas[0] ?? z.unknown();
22421
+ }
22422
+ if (schema.anyOf && Array.isArray(schema.anyOf)) {
22423
+ const schemas = schema.anyOf.map(jsonSchemaToZod);
22424
+ if (schemas.length >= 2) {
22425
+ return z.union(schemas);
22426
+ }
22427
+ return schemas[0] ?? z.unknown();
22428
+ }
22429
+ if (schema.allOf && Array.isArray(schema.allOf)) {
22430
+ const schemas = schema.allOf.map(jsonSchemaToZod);
22431
+ return schemas.reduce((acc, s) => z.intersection(acc, s));
22432
+ }
22433
+ const type = schema.type;
22434
+ const makeNullable = (s) => {
22435
+ if (schema.nullable === true) return s.nullable();
22436
+ return s;
22437
+ };
22438
+ switch (type) {
22439
+ case "string": {
22440
+ let s = z.string();
22441
+ if (schema.format) {
22442
+ switch (schema.format) {
22443
+ case "uri":
22444
+ case "url":
22445
+ s = z.string().url();
22446
+ break;
22447
+ case "email":
22448
+ s = z.string().email();
22449
+ break;
22450
+ case "date-time":
22451
+ case "datetime":
22452
+ s = z.string().datetime();
22453
+ break;
22454
+ }
22455
+ }
22456
+ if (typeof schema.minLength === "number") s = s.min(schema.minLength);
22457
+ if (typeof schema.maxLength === "number") s = s.max(schema.maxLength);
22458
+ return makeNullable(s);
22459
+ }
22460
+ case "number": {
22461
+ let n = z.number();
22462
+ if (typeof schema.minimum === "number") n = n.min(schema.minimum);
22463
+ if (typeof schema.maximum === "number") n = n.max(schema.maximum);
22464
+ if (typeof schema.exclusiveMinimum === "number") n = n.gt(schema.exclusiveMinimum);
22465
+ if (typeof schema.exclusiveMaximum === "number") n = n.lt(schema.exclusiveMaximum);
22466
+ return makeNullable(n);
22467
+ }
22468
+ case "integer": {
22469
+ let n = z.number().int();
22470
+ if (typeof schema.minimum === "number") n = n.min(schema.minimum);
22471
+ if (typeof schema.maximum === "number") n = n.max(schema.maximum);
22472
+ return makeNullable(n);
22473
+ }
22474
+ case "boolean":
22475
+ return makeNullable(z.boolean());
22476
+ case "null":
22477
+ return z.null();
22478
+ case "array":
22479
+ if (schema.items) {
22480
+ const itemSchema = jsonSchemaToZod(schema.items);
22481
+ let arr = z.array(itemSchema);
22482
+ if (typeof schema.minItems === "number") arr = arr.min(schema.minItems);
22483
+ if (typeof schema.maxItems === "number") arr = arr.max(schema.maxItems);
22484
+ return makeNullable(arr);
22485
+ }
22486
+ return makeNullable(z.array(z.unknown()));
22487
+ case "object": {
22488
+ const properties = schema.properties;
22489
+ const required = schema.required;
22490
+ if (!properties) {
22491
+ return makeNullable(z.record(z.string(), z.unknown()));
22492
+ }
22493
+ const shape = {};
22494
+ for (const [key, propSchema] of Object.entries(properties)) {
22495
+ let fieldSchema = jsonSchemaToZod(propSchema);
22496
+ if (!required?.includes(key)) {
22497
+ fieldSchema = fieldSchema.optional();
22498
+ }
22499
+ if (propSchema.description && typeof propSchema.description === "string") {
22500
+ fieldSchema = fieldSchema.describe(propSchema.description);
22501
+ }
22502
+ shape[key] = fieldSchema;
22503
+ }
22504
+ return makeNullable(z.object(shape));
22505
+ }
22506
+ default:
22507
+ if (Array.isArray(schema.type)) {
22508
+ const types = schema.type;
22509
+ if (types.includes("null")) {
22510
+ const nonNullType = types.find((t) => t !== "null");
22511
+ if (nonNullType) {
22512
+ return jsonSchemaToZod({ ...schema, type: nonNullType }).nullable();
22513
+ }
22514
+ }
22515
+ }
22516
+ return z.unknown();
22517
+ }
22518
+ }
22519
+ function createToolParametersSchema(tool) {
22520
+ const schema = tool.inputSchema;
22521
+ if (!schema || schema.type !== "object") {
22522
+ return z.object({});
22523
+ }
22524
+ return jsonSchemaToZod(schema);
22525
+ }
22526
+ function formatToolResult(result) {
22527
+ if (result.isError) {
22528
+ throw new Error(result.content.map((c) => c.text || "").join("\n"));
22529
+ }
22530
+ return result.content.map((item) => {
22531
+ switch (item.type) {
22532
+ case "text":
22533
+ return item.text || "";
22534
+ case "image":
22535
+ return `[Image: ${item.mimeType || "unknown"}]`;
22536
+ case "resource":
22537
+ return `[Resource: ${item.resource?.uri || "unknown"}]`;
22538
+ default:
22539
+ return "";
22540
+ }
22541
+ }).filter(Boolean).join("\n");
22542
+ }
22543
+ function createToolName(serverName, toolName, prefix) {
22544
+ return `${prefix}_${serverName}_${toolName}`.replace(/[^a-zA-Z0-9_]/g, "_");
22545
+ }
22546
+ function wrapMCPTool(tool, serverName, client, options = {}) {
22547
+ const opts = { ...DEFAULT_OPTIONS2, ...options };
22548
+ const wrappedName = createToolName(serverName, tool.name, opts.namePrefix);
22549
+ const parametersSchema = createToolParametersSchema(tool);
22550
+ const cocoTool = {
22551
+ name: wrappedName,
22552
+ description: buildMcpToolDescription(serverName, tool),
22553
+ category: opts.category,
22554
+ parameters: parametersSchema,
22555
+ execute: async (params) => {
22556
+ const timeout = opts.requestTimeout;
22557
+ try {
22558
+ const result = await Promise.race([
22559
+ client.callTool({
22560
+ name: tool.name,
22561
+ arguments: params
22562
+ }),
22563
+ new Promise((_, reject) => {
22564
+ setTimeout(() => {
22565
+ reject(new MCPTimeoutError(`Tool '${tool.name}' timed out after ${timeout}ms`));
22566
+ }, timeout);
22567
+ })
22568
+ ]);
22569
+ return formatToolResult(result);
22570
+ } catch (error) {
22571
+ if (error instanceof MCPError) {
22572
+ throw error;
22573
+ }
22574
+ throw new MCPError(
22575
+ -32603,
22576
+ `Tool execution failed: ${error instanceof Error ? error.message : "Unknown error"}`
22577
+ );
22578
+ }
22579
+ }
22580
+ };
22581
+ const wrapped = {
22582
+ originalTool: tool,
22583
+ serverName,
22584
+ wrappedName
22585
+ };
22586
+ return { tool: cocoTool, wrapped };
22587
+ }
22588
+ function wrapMCPTools(tools, serverName, client, options = {}) {
22589
+ const cocoTools = [];
22590
+ const wrappedTools = [];
22591
+ for (const tool of tools) {
22592
+ const { tool: cocoTool, wrapped } = wrapMCPTool(tool, serverName, client, options);
22593
+ cocoTools.push(cocoTool);
22594
+ wrappedTools.push(wrapped);
22595
+ }
22596
+ return { tools: cocoTools, wrapped: wrappedTools };
22597
+ }
22598
+ async function createToolsFromMCPServer(serverName, client, options = {}) {
22599
+ if (!client.isConnected()) {
22600
+ await client.initialize({
22601
+ protocolVersion: "2024-11-05",
22602
+ capabilities: {},
22603
+ clientInfo: { name: "coco-mcp-client", version: "0.2.0" }
22604
+ });
22605
+ }
22606
+ const { tools } = await client.listTools();
22607
+ return wrapMCPTools(tools, serverName, client, options);
22608
+ }
22609
+ async function registerMCPTools(registry, serverName, client, options = {}) {
22610
+ const { tools, wrapped } = await createToolsFromMCPServer(serverName, client, options);
22611
+ for (const tool of tools) {
22612
+ registry.register(tool);
22613
+ }
22614
+ return wrapped;
22615
+ }
22616
+ function getMCPToolInfo(wrappedName, wrappedTools) {
22617
+ return wrappedTools.find((t) => t.wrappedName === wrappedName);
22618
+ }
22619
+ function extractOriginalToolName(wrappedName, serverName, prefix = "mcp") {
22620
+ const prefix_pattern = `${prefix}_${serverName}_`;
22621
+ if (wrappedName.startsWith(prefix_pattern)) {
22622
+ return wrappedName.slice(prefix_pattern.length);
22623
+ }
22624
+ return null;
22625
+ }
22626
+ var DEFAULT_OPTIONS2;
22627
+ var init_tools = __esm({
22628
+ "src/mcp/tools.ts"() {
22629
+ init_errors2();
22630
+ DEFAULT_OPTIONS2 = {
22631
+ namePrefix: "mcp",
22632
+ category: "deploy",
22633
+ requestTimeout: 6e4
22634
+ };
22635
+ }
22636
+ });
22637
+
22344
22638
  // src/cli/repl/allow-path-prompt.ts
22345
22639
  var allow_path_prompt_exports = {};
22346
22640
  __export(allow_path_prompt_exports, {
@@ -22789,263 +23083,6 @@ var init_stack_detector = __esm({
22789
23083
  }
22790
23084
  });
22791
23085
 
22792
- // src/mcp/tools.ts
22793
- var tools_exports = {};
22794
- __export(tools_exports, {
22795
- createToolsFromMCPServer: () => createToolsFromMCPServer,
22796
- extractOriginalToolName: () => extractOriginalToolName,
22797
- getMCPToolInfo: () => getMCPToolInfo,
22798
- jsonSchemaToZod: () => jsonSchemaToZod,
22799
- registerMCPTools: () => registerMCPTools,
22800
- wrapMCPTool: () => wrapMCPTool,
22801
- wrapMCPTools: () => wrapMCPTools
22802
- });
22803
- function buildMcpToolDescription(serverName, tool) {
22804
- const base = tool.description || `Tool '${tool.name}' exposed by MCP server '${serverName}'`;
22805
- const lowerServer = serverName.toLowerCase();
22806
- if (lowerServer.includes("atlassian") || lowerServer.includes("jira") || lowerServer.includes("confluence")) {
22807
- return `${base}. Use this MCP tool for Atlassian/Jira/Confluence data. Prefer it over direct web_fetch or http_fetch for Atlassian content.`;
22808
- }
22809
- return `${base}. Exposed by MCP server '${serverName}'. Prefer this MCP tool over generic web/http fetch when accessing data from that connected service.`;
22810
- }
22811
- function jsonSchemaToZod(schema) {
22812
- if (schema.enum && Array.isArray(schema.enum)) {
22813
- const values = schema.enum;
22814
- if (values.length > 0 && values.every((v) => typeof v === "string")) {
22815
- return z.enum(values);
22816
- }
22817
- const literals = values.map((v) => z.literal(v));
22818
- if (literals.length < 2) {
22819
- return literals[0] ?? z.any();
22820
- }
22821
- return z.union(literals);
22822
- }
22823
- if (schema.const !== void 0) {
22824
- return z.literal(schema.const);
22825
- }
22826
- if (schema.oneOf && Array.isArray(schema.oneOf)) {
22827
- const schemas = schema.oneOf.map(jsonSchemaToZod);
22828
- if (schemas.length >= 2) {
22829
- return z.union(schemas);
22830
- }
22831
- return schemas[0] ?? z.unknown();
22832
- }
22833
- if (schema.anyOf && Array.isArray(schema.anyOf)) {
22834
- const schemas = schema.anyOf.map(jsonSchemaToZod);
22835
- if (schemas.length >= 2) {
22836
- return z.union(schemas);
22837
- }
22838
- return schemas[0] ?? z.unknown();
22839
- }
22840
- if (schema.allOf && Array.isArray(schema.allOf)) {
22841
- const schemas = schema.allOf.map(jsonSchemaToZod);
22842
- return schemas.reduce((acc, s) => z.intersection(acc, s));
22843
- }
22844
- const type = schema.type;
22845
- const makeNullable = (s) => {
22846
- if (schema.nullable === true) return s.nullable();
22847
- return s;
22848
- };
22849
- switch (type) {
22850
- case "string": {
22851
- let s = z.string();
22852
- if (schema.format) {
22853
- switch (schema.format) {
22854
- case "uri":
22855
- case "url":
22856
- s = z.string().url();
22857
- break;
22858
- case "email":
22859
- s = z.string().email();
22860
- break;
22861
- case "date-time":
22862
- case "datetime":
22863
- s = z.string().datetime();
22864
- break;
22865
- }
22866
- }
22867
- if (typeof schema.minLength === "number") s = s.min(schema.minLength);
22868
- if (typeof schema.maxLength === "number") s = s.max(schema.maxLength);
22869
- return makeNullable(s);
22870
- }
22871
- case "number": {
22872
- let n = z.number();
22873
- if (typeof schema.minimum === "number") n = n.min(schema.minimum);
22874
- if (typeof schema.maximum === "number") n = n.max(schema.maximum);
22875
- if (typeof schema.exclusiveMinimum === "number") n = n.gt(schema.exclusiveMinimum);
22876
- if (typeof schema.exclusiveMaximum === "number") n = n.lt(schema.exclusiveMaximum);
22877
- return makeNullable(n);
22878
- }
22879
- case "integer": {
22880
- let n = z.number().int();
22881
- if (typeof schema.minimum === "number") n = n.min(schema.minimum);
22882
- if (typeof schema.maximum === "number") n = n.max(schema.maximum);
22883
- return makeNullable(n);
22884
- }
22885
- case "boolean":
22886
- return makeNullable(z.boolean());
22887
- case "null":
22888
- return z.null();
22889
- case "array":
22890
- if (schema.items) {
22891
- const itemSchema = jsonSchemaToZod(schema.items);
22892
- let arr = z.array(itemSchema);
22893
- if (typeof schema.minItems === "number") arr = arr.min(schema.minItems);
22894
- if (typeof schema.maxItems === "number") arr = arr.max(schema.maxItems);
22895
- return makeNullable(arr);
22896
- }
22897
- return makeNullable(z.array(z.unknown()));
22898
- case "object": {
22899
- const properties = schema.properties;
22900
- const required = schema.required;
22901
- if (!properties) {
22902
- return makeNullable(z.record(z.string(), z.unknown()));
22903
- }
22904
- const shape = {};
22905
- for (const [key, propSchema] of Object.entries(properties)) {
22906
- let fieldSchema = jsonSchemaToZod(propSchema);
22907
- if (!required?.includes(key)) {
22908
- fieldSchema = fieldSchema.optional();
22909
- }
22910
- if (propSchema.description && typeof propSchema.description === "string") {
22911
- fieldSchema = fieldSchema.describe(propSchema.description);
22912
- }
22913
- shape[key] = fieldSchema;
22914
- }
22915
- return makeNullable(z.object(shape));
22916
- }
22917
- default:
22918
- if (Array.isArray(schema.type)) {
22919
- const types = schema.type;
22920
- if (types.includes("null")) {
22921
- const nonNullType = types.find((t) => t !== "null");
22922
- if (nonNullType) {
22923
- return jsonSchemaToZod({ ...schema, type: nonNullType }).nullable();
22924
- }
22925
- }
22926
- }
22927
- return z.unknown();
22928
- }
22929
- }
22930
- function createToolParametersSchema(tool) {
22931
- const schema = tool.inputSchema;
22932
- if (!schema || schema.type !== "object") {
22933
- return z.object({});
22934
- }
22935
- return jsonSchemaToZod(schema);
22936
- }
22937
- function formatToolResult(result) {
22938
- if (result.isError) {
22939
- throw new Error(result.content.map((c) => c.text || "").join("\n"));
22940
- }
22941
- return result.content.map((item) => {
22942
- switch (item.type) {
22943
- case "text":
22944
- return item.text || "";
22945
- case "image":
22946
- return `[Image: ${item.mimeType || "unknown"}]`;
22947
- case "resource":
22948
- return `[Resource: ${item.resource?.uri || "unknown"}]`;
22949
- default:
22950
- return "";
22951
- }
22952
- }).filter(Boolean).join("\n");
22953
- }
22954
- function createToolName(serverName, toolName, prefix) {
22955
- return `${prefix}_${serverName}_${toolName}`.replace(/[^a-zA-Z0-9_]/g, "_");
22956
- }
22957
- function wrapMCPTool(tool, serverName, client, options = {}) {
22958
- const opts = { ...DEFAULT_OPTIONS2, ...options };
22959
- const wrappedName = createToolName(serverName, tool.name, opts.namePrefix);
22960
- const parametersSchema = createToolParametersSchema(tool);
22961
- const cocoTool = {
22962
- name: wrappedName,
22963
- description: buildMcpToolDescription(serverName, tool),
22964
- category: opts.category,
22965
- parameters: parametersSchema,
22966
- execute: async (params) => {
22967
- const timeout = opts.requestTimeout;
22968
- try {
22969
- const result = await Promise.race([
22970
- client.callTool({
22971
- name: tool.name,
22972
- arguments: params
22973
- }),
22974
- new Promise((_, reject) => {
22975
- setTimeout(() => {
22976
- reject(new MCPTimeoutError(`Tool '${tool.name}' timed out after ${timeout}ms`));
22977
- }, timeout);
22978
- })
22979
- ]);
22980
- return formatToolResult(result);
22981
- } catch (error) {
22982
- if (error instanceof MCPError) {
22983
- throw error;
22984
- }
22985
- throw new MCPError(
22986
- -32603,
22987
- `Tool execution failed: ${error instanceof Error ? error.message : "Unknown error"}`
22988
- );
22989
- }
22990
- }
22991
- };
22992
- const wrapped = {
22993
- originalTool: tool,
22994
- serverName,
22995
- wrappedName
22996
- };
22997
- return { tool: cocoTool, wrapped };
22998
- }
22999
- function wrapMCPTools(tools, serverName, client, options = {}) {
23000
- const cocoTools = [];
23001
- const wrappedTools = [];
23002
- for (const tool of tools) {
23003
- const { tool: cocoTool, wrapped } = wrapMCPTool(tool, serverName, client, options);
23004
- cocoTools.push(cocoTool);
23005
- wrappedTools.push(wrapped);
23006
- }
23007
- return { tools: cocoTools, wrapped: wrappedTools };
23008
- }
23009
- async function createToolsFromMCPServer(serverName, client, options = {}) {
23010
- if (!client.isConnected()) {
23011
- await client.initialize({
23012
- protocolVersion: "2024-11-05",
23013
- capabilities: {},
23014
- clientInfo: { name: "coco-mcp-client", version: "0.2.0" }
23015
- });
23016
- }
23017
- const { tools } = await client.listTools();
23018
- return wrapMCPTools(tools, serverName, client, options);
23019
- }
23020
- async function registerMCPTools(registry, serverName, client, options = {}) {
23021
- const { tools, wrapped } = await createToolsFromMCPServer(serverName, client, options);
23022
- for (const tool of tools) {
23023
- registry.register(tool);
23024
- }
23025
- return wrapped;
23026
- }
23027
- function getMCPToolInfo(wrappedName, wrappedTools) {
23028
- return wrappedTools.find((t) => t.wrappedName === wrappedName);
23029
- }
23030
- function extractOriginalToolName(wrappedName, serverName, prefix = "mcp") {
23031
- const prefix_pattern = `${prefix}_${serverName}_`;
23032
- if (wrappedName.startsWith(prefix_pattern)) {
23033
- return wrappedName.slice(prefix_pattern.length);
23034
- }
23035
- return null;
23036
- }
23037
- var DEFAULT_OPTIONS2;
23038
- var init_tools = __esm({
23039
- "src/mcp/tools.ts"() {
23040
- init_errors2();
23041
- DEFAULT_OPTIONS2 = {
23042
- namePrefix: "mcp",
23043
- category: "deploy",
23044
- requestTimeout: 6e4
23045
- };
23046
- }
23047
- });
23048
-
23049
23086
  // src/cli/repl/hooks/types.ts
23050
23087
  function isHookEvent(value) {
23051
23088
  return typeof value === "string" && HOOK_EVENTS.includes(value);
@@ -34614,8 +34651,9 @@ ${newProvider.emoji} ${newProvider.name} is not configured.`));
34614
34651
  newApiKeyForSaving = key;
34615
34652
  }
34616
34653
  }
34654
+ const rememberedModel = await getLastUsedModel(newProvider.id);
34617
34655
  const recommendedModel = getRecommendedModel(newProvider.id);
34618
- const newModel = recommendedModel?.id || newProvider.models[0]?.id || "";
34656
+ const newModel = rememberedModel || recommendedModel?.id || newProvider.models[0]?.id || "";
34619
34657
  const spinner18 = p26.spinner();
34620
34658
  spinner18.start(`Connecting to ${newProvider.name}...`);
34621
34659
  try {
@@ -38270,6 +38308,9 @@ var RECOMMENDED_DENY = [
38270
38308
  "bash:eval",
38271
38309
  "bash:source"
38272
38310
  ];
38311
+ function getProjectPreferenceKey(projectPath) {
38312
+ return path39__default.resolve(projectPath);
38313
+ }
38273
38314
  async function loadPermissionPreferences() {
38274
38315
  try {
38275
38316
  const content = await fs35__default.readFile(CONFIG_PATHS.config, "utf-8");
@@ -38277,7 +38318,8 @@ async function loadPermissionPreferences() {
38277
38318
  return {
38278
38319
  recommendedAllowlistApplied: config.recommendedAllowlistApplied,
38279
38320
  recommendedAllowlistDismissed: config.recommendedAllowlistDismissed,
38280
- recommendedAllowlistPrompted: config.recommendedAllowlistPrompted
38321
+ recommendedAllowlistPrompted: config.recommendedAllowlistPrompted,
38322
+ recommendedAllowlistPromptedProjects: config.recommendedAllowlistPromptedProjects
38281
38323
  };
38282
38324
  } catch {
38283
38325
  return {};
@@ -38297,7 +38339,26 @@ async function savePermissionPreference(key, value) {
38297
38339
  } catch {
38298
38340
  }
38299
38341
  }
38300
- async function shouldShowPermissionSuggestion() {
38342
+ async function markPermissionSuggestionShownForProject(projectPath) {
38343
+ try {
38344
+ let config = {};
38345
+ try {
38346
+ const content = await fs35__default.readFile(CONFIG_PATHS.config, "utf-8");
38347
+ config = JSON.parse(content);
38348
+ } catch {
38349
+ }
38350
+ const promptedProjects = {
38351
+ ...config.recommendedAllowlistPromptedProjects,
38352
+ [getProjectPreferenceKey(projectPath)]: true
38353
+ };
38354
+ config.recommendedAllowlistPromptedProjects = promptedProjects;
38355
+ config.recommendedAllowlistPrompted = true;
38356
+ await fs35__default.mkdir(path39__default.dirname(CONFIG_PATHS.config), { recursive: true });
38357
+ await fs35__default.writeFile(CONFIG_PATHS.config, JSON.stringify(config, null, 2), "utf-8");
38358
+ } catch {
38359
+ }
38360
+ }
38361
+ async function shouldShowPermissionSuggestion(projectPath = process.cwd()) {
38301
38362
  const prefs = await loadPermissionPreferences();
38302
38363
  if (prefs.recommendedAllowlistDismissed) {
38303
38364
  return false;
@@ -38305,6 +38366,10 @@ async function shouldShowPermissionSuggestion() {
38305
38366
  if (prefs.recommendedAllowlistApplied) {
38306
38367
  return false;
38307
38368
  }
38369
+ const projectKey = getProjectPreferenceKey(projectPath);
38370
+ if (prefs.recommendedAllowlistPromptedProjects?.[projectKey]) {
38371
+ return false;
38372
+ }
38308
38373
  return true;
38309
38374
  }
38310
38375
  async function applyRecommendedPermissions() {
@@ -38313,7 +38378,8 @@ async function applyRecommendedPermissions() {
38313
38378
  }
38314
38379
  await savePermissionPreference("recommendedAllowlistApplied", true);
38315
38380
  }
38316
- async function showPermissionSuggestion() {
38381
+ async function showPermissionSuggestion(projectPath = process.cwd()) {
38382
+ await markPermissionSuggestionShownForProject(projectPath);
38317
38383
  console.log();
38318
38384
  console.log(chalk.magenta.bold(" \u{1F4CB} Recommended Permissions"));
38319
38385
  console.log();
@@ -47369,6 +47435,27 @@ init_registry4();
47369
47435
  init_registry();
47370
47436
  init_config_loader();
47371
47437
  init_lifecycle();
47438
+ init_tools();
47439
+ async function loadConfiguredServers(projectPath) {
47440
+ const registry = new MCPRegistryImpl();
47441
+ await registry.load();
47442
+ const resolvedProjectPath = projectPath || process.cwd();
47443
+ return mergeMCPConfigs(
47444
+ registry.listServers(),
47445
+ await loadMCPServersFromCOCOConfig(),
47446
+ await loadProjectMCPFile(resolvedProjectPath)
47447
+ );
47448
+ }
47449
+ function findConfiguredServer(servers, requestedServer) {
47450
+ const normalized = requestedServer.trim().toLowerCase();
47451
+ return servers.find((server) => {
47452
+ const name = server.name.toLowerCase();
47453
+ if (name === normalized) return true;
47454
+ if (name.includes(normalized) || normalized.includes(name)) return true;
47455
+ if (name.includes("atlassian") && /^(atlassian|jira|confluence)$/.test(normalized)) return true;
47456
+ return false;
47457
+ });
47458
+ }
47372
47459
  var mcpListServersTool = defineTool({
47373
47460
  name: "mcp_list_servers",
47374
47461
  description: `Inspect Coco's MCP configuration and current runtime connections.
@@ -47382,14 +47469,9 @@ when you need to know which MCP servers are configured, connected, healthy, or w
47382
47469
  projectPath: z.string().optional().describe("Project path whose .mcp.json should be merged. Defaults to process.cwd()")
47383
47470
  }),
47384
47471
  async execute({ includeDisabled, includeTools, projectPath }) {
47385
- const registry = new MCPRegistryImpl();
47386
- await registry.load();
47387
- const resolvedProjectPath = projectPath || process.cwd();
47388
- const configuredServers = mergeMCPConfigs(
47389
- registry.listServers(),
47390
- await loadMCPServersFromCOCOConfig(),
47391
- await loadProjectMCPFile(resolvedProjectPath)
47392
- ).filter((server) => includeDisabled || server.enabled !== false);
47472
+ const configuredServers = (await loadConfiguredServers(projectPath)).filter(
47473
+ (server) => includeDisabled || server.enabled !== false
47474
+ );
47393
47475
  const manager = getMCPServerManager();
47394
47476
  const servers = [];
47395
47477
  for (const server of configuredServers) {
@@ -47420,7 +47502,59 @@ when you need to know which MCP servers are configured, connected, healthy, or w
47420
47502
  };
47421
47503
  }
47422
47504
  });
47423
- var mcpTools = [mcpListServersTool];
47505
+ var mcpConnectServerTool = defineTool({
47506
+ name: "mcp_connect_server",
47507
+ description: `Connect or reconnect a configured MCP server in the current Coco session.
47508
+
47509
+ Use this when mcp_list_servers shows a service as configured but disconnected, or when
47510
+ the user explicitly asks you to use a specific MCP service. This tool can trigger the
47511
+ built-in MCP OAuth browser login flow. Do not ask the user for raw tokens when this exists.`,
47512
+ category: "config",
47513
+ parameters: z.object({
47514
+ server: z.string().describe("Configured MCP server name, or a common alias like 'jira' or 'atlassian'"),
47515
+ includeTools: z.boolean().optional().default(true).describe("Include discovered MCP tool names after connecting"),
47516
+ projectPath: z.string().optional().describe("Project path whose .mcp.json should be merged. Defaults to process.cwd()")
47517
+ }),
47518
+ async execute({ server, includeTools, projectPath }) {
47519
+ const configuredServers = await loadConfiguredServers(projectPath);
47520
+ const target = findConfiguredServer(
47521
+ configuredServers.filter((configuredServer) => configuredServer.enabled !== false),
47522
+ server
47523
+ );
47524
+ if (!target) {
47525
+ throw new Error(`MCP server '${server}' is not configured`);
47526
+ }
47527
+ const manager = getMCPServerManager();
47528
+ const existingConnection = manager.getConnection(target.name);
47529
+ if (existingConnection && existingConnection.healthy === false) {
47530
+ await manager.stopServer(target.name);
47531
+ }
47532
+ const connection = await manager.startServer(target);
47533
+ const toolRegistry = getAgentToolRegistry();
47534
+ if (toolRegistry) {
47535
+ await registerMCPTools(toolRegistry, connection.name, connection.client);
47536
+ }
47537
+ let tools;
47538
+ if (includeTools) {
47539
+ try {
47540
+ const listed = await connection.client.listTools();
47541
+ tools = listed.tools.map((tool) => tool.name);
47542
+ } catch {
47543
+ tools = [];
47544
+ }
47545
+ }
47546
+ return {
47547
+ requestedServer: server,
47548
+ connected: true,
47549
+ healthy: true,
47550
+ toolCount: connection.toolCount,
47551
+ ...includeTools ? { tools: tools ?? [] } : {},
47552
+ authTriggered: target.transport === "http",
47553
+ message: `MCP server '${target.name}' is connected for this session.`
47554
+ };
47555
+ }
47556
+ });
47557
+ var mcpTools = [mcpListServersTool, mcpConnectServerTool];
47424
47558
 
47425
47559
  // src/tools/index.ts
47426
47560
  init_registry4();
@@ -53037,8 +53171,8 @@ async function startRepl(options = {}) {
53037
53171
  ]);
53038
53172
  session.projectContext = projectContext;
53039
53173
  await initializeSessionMemory(session);
53040
- if (await shouldShowPermissionSuggestion()) {
53041
- await showPermissionSuggestion();
53174
+ if (await shouldShowPermissionSuggestion(projectPath)) {
53175
+ await showPermissionSuggestion(projectPath);
53042
53176
  const updatedTrust = await loadTrustedTools(projectPath);
53043
53177
  for (const tool of updatedTrust) {
53044
53178
  session.trustedTools.add(tool);
@@ -53074,6 +53208,8 @@ async function startRepl(options = {}) {
53074
53208
  );
53075
53209
  }
53076
53210
  let mcpManager = null;
53211
+ let configuredMcpServers = [];
53212
+ const registeredMcpServers = /* @__PURE__ */ new Set();
53077
53213
  const logger2 = (await Promise.resolve().then(() => (init_logger(), logger_exports))).getLogger();
53078
53214
  try {
53079
53215
  const { getMCPServerManager: getMCPServerManager2 } = await Promise.resolve().then(() => (init_lifecycle(), lifecycle_exports));
@@ -53090,6 +53226,7 @@ async function startRepl(options = {}) {
53090
53226
  cocoConfigServers.filter((s) => s.enabled !== false),
53091
53227
  projectServers.filter((s) => s.enabled !== false)
53092
53228
  );
53229
+ configuredMcpServers = enabledServers;
53093
53230
  if (enabledServers.length > 0) {
53094
53231
  mcpManager = getMCPServerManager2();
53095
53232
  let connections;
@@ -53109,6 +53246,7 @@ async function startRepl(options = {}) {
53109
53246
  for (const connection of connections.values()) {
53110
53247
  try {
53111
53248
  const wrapped = await registerMCPTools2(toolRegistry, connection.name, connection.client);
53249
+ registeredMcpServers.add(connection.name);
53112
53250
  if (wrapped.length === 0) {
53113
53251
  logger2.warn(
53114
53252
  `[MCP] Server '${connection.name}' connected but exposed 0 tools (check server auth/scopes).`
@@ -53137,6 +53275,46 @@ async function startRepl(options = {}) {
53137
53275
  `[MCP] Initialization failed: ${mcpError instanceof Error ? mcpError.message : String(mcpError)}`
53138
53276
  );
53139
53277
  }
53278
+ async function ensureRequestedMcpConnections(message) {
53279
+ if (configuredMcpServers.length === 0) return;
53280
+ if (!mcpManager) {
53281
+ const { getMCPServerManager: getMCPServerManager2 } = await Promise.resolve().then(() => (init_lifecycle(), lifecycle_exports));
53282
+ mcpManager = getMCPServerManager2();
53283
+ }
53284
+ const normalizedMessage = message.toLowerCase();
53285
+ const explicitlyRequestsMcp = /\bmcp\b/.test(normalizedMessage) || /\b(use|using|usa|usar|utiliza|utilizar)\b.{0,24}\bmcp\b/.test(normalizedMessage);
53286
+ const matchingServers = configuredMcpServers.filter((server) => {
53287
+ if (mcpManager?.getConnection(server.name)) return false;
53288
+ if (explicitlyRequestsMcp) return true;
53289
+ const loweredName = server.name.toLowerCase();
53290
+ if (normalizedMessage.includes(loweredName)) return true;
53291
+ if (loweredName.includes("atlassian")) {
53292
+ return /\b(atlassian|jira|confluence)\b/.test(normalizedMessage);
53293
+ }
53294
+ return false;
53295
+ });
53296
+ for (const server of matchingServers) {
53297
+ try {
53298
+ const existingConnection = mcpManager.getConnection(server.name);
53299
+ if (existingConnection?.healthy === false) {
53300
+ await mcpManager.stopServer(server.name);
53301
+ }
53302
+ const connection = await mcpManager.startServer(server);
53303
+ if (!registeredMcpServers.has(connection.name)) {
53304
+ await (await Promise.resolve().then(() => (init_tools(), tools_exports))).registerMCPTools(toolRegistry, connection.name, connection.client);
53305
+ registeredMcpServers.add(connection.name);
53306
+ }
53307
+ } catch (error) {
53308
+ logger2.warn(
53309
+ `[MCP] On-demand connect failed for '${server.name}': ${error instanceof Error ? error.message : String(error)}`
53310
+ );
53311
+ }
53312
+ }
53313
+ }
53314
+ function extractMessageText(content) {
53315
+ if (typeof content === "string") return content;
53316
+ return content.filter((block) => block.type === "text").map((block) => block.text).join(" ");
53317
+ }
53140
53318
  let hookRegistry;
53141
53319
  let hookExecutor;
53142
53320
  try {
@@ -53584,6 +53762,7 @@ ${imagePrompts}`.trim() : imagePrompts;
53584
53762
  let streamStarted = false;
53585
53763
  let llmCallCount = 0;
53586
53764
  let lastToolGroup = null;
53765
+ await ensureRequestedMcpConnections(extractMessageText(effectiveMessage));
53587
53766
  const result = await executeAgentTurn(session, effectiveMessage, provider, toolRegistry, {
53588
53767
  onStream: (chunk) => {
53589
53768
  if (!streamStarted) {