@ateam-ai/mcp 0.1.2 → 0.1.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ateam-ai/mcp",
3
- "version": "0.1.2",
3
+ "version": "0.1.5",
4
4
  "mcpName": "io.github.ariekogan/ateam-mcp",
5
5
  "description": "ADAS MCP Server — build, validate, and deploy multi-agent solutions from any AI environment",
6
6
  "type": "module",
package/src/api.js CHANGED
@@ -1,50 +1,179 @@
1
1
  /**
2
2
  * ADAS API client — thin HTTP wrapper for the External Agent API.
3
+ *
4
+ * Credentials resolve in this order:
5
+ * 1. Per-session override (set via adas_auth tool — used by HTTP transport)
6
+ * 2. Environment variables (ADAS_API_KEY, ADAS_TENANT — used by stdio transport)
7
+ * 3. Defaults (no key, tenant "main")
3
8
  */
4
9
 
5
10
  const BASE_URL = process.env.ADAS_API_URL || "https://api.ateam-ai.com";
6
- const TENANT = process.env.ADAS_TENANT || "main";
7
- const API_KEY = process.env.ADAS_API_KEY || "";
8
-
9
- function headers() {
10
- return {
11
- "Content-Type": "application/json",
12
- "X-ADAS-TENANT": TENANT,
13
- "X-API-KEY": API_KEY,
11
+ const ENV_TENANT = process.env.ADAS_TENANT || "";
12
+ const ENV_API_KEY = process.env.ADAS_API_KEY || "";
13
+
14
+ // Request timeout (30 seconds)
15
+ const REQUEST_TIMEOUT_MS = 30_000;
16
+
17
+ // Per-session credential store (sessionId → { tenant, apiKey })
18
+ const sessions = new Map();
19
+
20
+ /**
21
+ * Parse a tenant-embedded API key.
22
+ * Format: adas_<tenant>_<32hex>
23
+ * Legacy: adas_<32hex> (no tenant embedded)
24
+ * @returns {{ tenant: string|null, isValid: boolean }}
25
+ */
26
+ export function parseApiKey(key) {
27
+ if (!key || typeof key !== 'string') return { tenant: null, isValid: false };
28
+ const match = key.match(/^adas_([a-z0-9][a-z0-9-]{0,28}[a-z0-9])_([0-9a-f]{32})$/);
29
+ if (match) return { tenant: match[1], isValid: true };
30
+ const legacy = key.match(/^adas_([0-9a-f]{32})$/);
31
+ if (legacy) return { tenant: null, isValid: true };
32
+ return { tenant: null, isValid: false };
33
+ }
34
+
35
+ /**
36
+ * Set credentials for a session (called by adas_auth tool).
37
+ * If tenant is not provided, it's auto-extracted from the key.
38
+ */
39
+ export function setSessionCredentials(sessionId, { tenant, apiKey }) {
40
+ let resolvedTenant = tenant;
41
+ if (!resolvedTenant && apiKey) {
42
+ const parsed = parseApiKey(apiKey);
43
+ if (parsed.tenant) resolvedTenant = parsed.tenant;
44
+ }
45
+ sessions.set(sessionId, { tenant: resolvedTenant || "main", apiKey });
46
+ }
47
+
48
+ /**
49
+ * Get credentials for a session, falling back to env vars.
50
+ * If using env var key with embedded tenant and no explicit ADAS_TENANT, auto-extract.
51
+ */
52
+ export function getCredentials(sessionId) {
53
+ const session = sessionId ? sessions.get(sessionId) : null;
54
+ if (session) {
55
+ return { tenant: session.tenant, apiKey: session.apiKey };
56
+ }
57
+ const apiKey = ENV_API_KEY || "";
58
+ let tenant = ENV_TENANT;
59
+ if (!tenant && apiKey) {
60
+ const parsed = parseApiKey(apiKey);
61
+ if (parsed.tenant) tenant = parsed.tenant;
62
+ }
63
+ return { tenant: tenant || "main", apiKey };
64
+ }
65
+
66
+ /**
67
+ * Check if a session is authenticated (has an API key from any source).
68
+ */
69
+ export function isAuthenticated(sessionId) {
70
+ const { apiKey } = getCredentials(sessionId);
71
+ return apiKey.length > 0;
72
+ }
73
+
74
+ /**
75
+ * Remove session credentials (on disconnect).
76
+ */
77
+ export function clearSession(sessionId) {
78
+ sessions.delete(sessionId);
79
+ }
80
+
81
+ function headers(sessionId) {
82
+ const { tenant, apiKey } = getCredentials(sessionId);
83
+ const h = { "Content-Type": "application/json" };
84
+ if (tenant) h["X-ADAS-TENANT"] = tenant;
85
+ if (apiKey) h["X-API-KEY"] = apiKey;
86
+ return h;
87
+ }
88
+
89
+ /**
90
+ * Format an API error into a user-friendly message with actionable hints.
91
+ */
92
+ function formatError(method, path, status, body) {
93
+ const hints = {
94
+ 401: "Your API key may be invalid or expired. Try calling adas_auth again with a valid key.",
95
+ 403: "You don't have permission for this operation. Check your tenant and API key.",
96
+ 404: "Resource not found. Check the solution_id or skill_id you're using. Use adas_list_solutions to see available solutions.",
97
+ 409: "Conflict — the resource may already exist or is in a conflicting state.",
98
+ 422: "Validation failed. Check the request payload against the spec (use adas_get_spec).",
99
+ 429: "Rate limited. Wait a moment and try again.",
100
+ 500: "ADAS server error. The platform may be temporarily unavailable. Try again in a minute.",
101
+ 502: "ADAS API is unreachable. The service may be restarting. Try again in a minute.",
102
+ 503: "ADAS API is temporarily unavailable. Try again in a minute.",
14
103
  };
104
+
105
+ const hint = hints[status] || "";
106
+ const detail = typeof body === "string" && body.length > 0 && body.length < 500 ? body : "";
107
+
108
+ let msg = `ADAS API error: ${method} ${path} returned ${status}`;
109
+ if (detail) msg += ` — ${detail}`;
110
+ if (hint) msg += `\nHint: ${hint}`;
111
+
112
+ return msg;
113
+ }
114
+
115
+ /**
116
+ * Core fetch wrapper with timeout and error formatting.
117
+ */
118
+ async function request(method, path, body, sessionId) {
119
+ const controller = new AbortController();
120
+ const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
121
+
122
+ try {
123
+ const opts = {
124
+ method,
125
+ headers: headers(sessionId),
126
+ signal: controller.signal,
127
+ };
128
+ if (body !== undefined) {
129
+ opts.body = JSON.stringify(body);
130
+ }
131
+
132
+ const res = await fetch(`${BASE_URL}${path}`, opts);
133
+
134
+ if (!res.ok) {
135
+ const text = await res.text().catch(() => "");
136
+ throw new Error(formatError(method, path, res.status, text));
137
+ }
138
+
139
+ return res.json();
140
+ } catch (err) {
141
+ if (err.name === "AbortError") {
142
+ throw new Error(
143
+ `ADAS API timeout: ${method} ${path} did not respond within ${REQUEST_TIMEOUT_MS / 1000}s.\n` +
144
+ `Hint: The ADAS API at ${BASE_URL} may be down. Check https://api.ateam-ai.com/health`
145
+ );
146
+ }
147
+ if (err.cause?.code === "ECONNREFUSED") {
148
+ throw new Error(
149
+ `Cannot connect to ADAS API at ${BASE_URL}.\n` +
150
+ `Hint: The service may be down. Check https://api.ateam-ai.com/health`
151
+ );
152
+ }
153
+ if (err.cause?.code === "ENOTFOUND") {
154
+ throw new Error(
155
+ `Cannot resolve ADAS API host: ${BASE_URL}.\n` +
156
+ `Hint: Check your internet connection and ADAS_API_URL setting.`
157
+ );
158
+ }
159
+ throw err;
160
+ } finally {
161
+ clearTimeout(timeout);
162
+ }
15
163
  }
16
164
 
17
- export async function get(path) {
18
- const res = await fetch(`${BASE_URL}${path}`, { headers: headers() });
19
- if (!res.ok) throw new Error(`GET ${path} → ${res.status}: ${await res.text()}`);
20
- return res.json();
165
+ export async function get(path, sessionId) {
166
+ return request("GET", path, undefined, sessionId);
21
167
  }
22
168
 
23
- export async function post(path, body) {
24
- const res = await fetch(`${BASE_URL}${path}`, {
25
- method: "POST",
26
- headers: headers(),
27
- body: JSON.stringify(body),
28
- });
29
- if (!res.ok) throw new Error(`POST ${path} → ${res.status}: ${await res.text()}`);
30
- return res.json();
169
+ export async function post(path, body, sessionId) {
170
+ return request("POST", path, body, sessionId);
31
171
  }
32
172
 
33
- export async function patch(path, body) {
34
- const res = await fetch(`${BASE_URL}${path}`, {
35
- method: "PATCH",
36
- headers: headers(),
37
- body: JSON.stringify(body),
38
- });
39
- if (!res.ok) throw new Error(`PATCH ${path} → ${res.status}: ${await res.text()}`);
40
- return res.json();
173
+ export async function patch(path, body, sessionId) {
174
+ return request("PATCH", path, body, sessionId);
41
175
  }
42
176
 
43
- export async function del(path) {
44
- const res = await fetch(`${BASE_URL}${path}`, {
45
- method: "DELETE",
46
- headers: headers(),
47
- });
48
- if (!res.ok) throw new Error(`DELETE ${path} → ${res.status}: ${await res.text()}`);
49
- return res.json();
177
+ export async function del(path, sessionId) {
178
+ return request("DELETE", path, undefined, sessionId);
50
179
  }
package/src/http.js CHANGED
@@ -8,6 +8,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
8
8
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
9
9
  import express from "express";
10
10
  import { createServer } from "./server.js";
11
+ import { clearSession } from "./api.js";
11
12
 
12
13
  // Active sessions
13
14
  const transports = {};
@@ -21,6 +22,11 @@ export function startHttpServer(port = 3100) {
21
22
  res.json({ ok: true, service: "ateam-mcp", transport: "http" });
22
23
  });
23
24
 
25
+ // ─── Get API Key — redirect to Skill Builder with auto-open ──
26
+ app.get("/get-api-key", (_req, res) => {
27
+ res.redirect("https://app.ateam-ai.com/builder/?show=api-key");
28
+ });
29
+
24
30
  // ─── MCP POST — handle tool calls + initialize ───────────────
25
31
  app.post("/mcp", async (req, res) => {
26
32
  const sessionId = req.headers["mcp-session-id"];
@@ -32,9 +38,11 @@ export function startHttpServer(port = 3100) {
32
38
  // Reuse existing session
33
39
  transport = transports[sessionId];
34
40
  } else if (!sessionId && isInitializeRequest(req.body)) {
35
- // New session
41
+ // New session — generate ID upfront so we can bind it to the server
42
+ const newSessionId = randomUUID();
43
+
36
44
  transport = new StreamableHTTPServerTransport({
37
- sessionIdGenerator: () => randomUUID(),
45
+ sessionIdGenerator: () => newSessionId,
38
46
  onsessioninitialized: (sid) => {
39
47
  transports[sid] = transport;
40
48
  },
@@ -42,12 +50,13 @@ export function startHttpServer(port = 3100) {
42
50
 
43
51
  transport.onclose = () => {
44
52
  const sid = transport.sessionId;
45
- if (sid && transports[sid]) {
53
+ if (sid) {
46
54
  delete transports[sid];
55
+ clearSession(sid); // drop per-session credentials
47
56
  }
48
57
  };
49
58
 
50
- const server = createServer();
59
+ const server = createServer(newSessionId);
51
60
  await server.connect(transport);
52
61
  await transport.handleRequest(req, res, req.body);
53
62
  return;
@@ -107,6 +116,7 @@ export function startHttpServer(port = 3100) {
107
116
  await transports[sid].close();
108
117
  } catch {}
109
118
  delete transports[sid];
119
+ clearSession(sid);
110
120
  }
111
121
  process.exit(0);
112
122
  });
package/src/server.js CHANGED
@@ -1,5 +1,8 @@
1
1
  /**
2
2
  * Shared MCP server factory — used by both stdio and HTTP transports.
3
+ *
4
+ * Each server instance is bound to a sessionId so that tool handlers
5
+ * can resolve per-session credentials (set via the adas_auth tool).
3
6
  */
4
7
 
5
8
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -9,9 +12,13 @@ import {
9
12
  } from "@modelcontextprotocol/sdk/types.js";
10
13
  import { tools, handleToolCall } from "./tools.js";
11
14
 
12
- export function createServer() {
15
+ /**
16
+ * @param {string} sessionId — identifier for credential isolation.
17
+ * HTTP transport passes the MCP session UUID; stdio uses "stdio".
18
+ */
19
+ export function createServer(sessionId = "stdio") {
13
20
  const server = new Server(
14
- { name: "ateam-mcp", version: "0.1.0" },
21
+ { name: "ateam-mcp", version: "0.1.5" },
15
22
  { capabilities: { tools: {} } }
16
23
  );
17
24
 
@@ -19,7 +26,7 @@ export function createServer() {
19
26
 
20
27
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
21
28
  const { name, arguments: args } = request.params;
22
- return handleToolCall(name, args);
29
+ return handleToolCall(name, args, sessionId);
23
30
  });
24
31
 
25
32
  return server;
package/src/tools.js CHANGED
@@ -1,17 +1,48 @@
1
1
  /**
2
2
  * ADAS MCP tool definitions and handlers.
3
- * 12 tools covering the full ADAS External Agent API.
3
+ * 15 tools covering the full ADAS External Agent API + auth + bootstrap.
4
4
  */
5
5
 
6
- import { get, post, patch, del } from "./api.js";
6
+ import {
7
+ get, post, patch, del,
8
+ setSessionCredentials, isAuthenticated, getCredentials, parseApiKey,
9
+ } from "./api.js";
7
10
 
8
11
  // ─── Tool definitions ───────────────────────────────────────────────
9
12
 
10
13
  export const tools = [
14
+ {
15
+ name: "adas_bootstrap",
16
+ description:
17
+ "Call this FIRST after installing A-Team MCP. Returns a structured overview of what A-Team is, what the user can build, and the recommended step-by-step flow. Includes the first questions to ask the user and suggested next tool calls.",
18
+ inputSchema: {
19
+ type: "object",
20
+ properties: {},
21
+ },
22
+ },
23
+ {
24
+ name: "adas_auth",
25
+ description:
26
+ "Authenticate with ADAS. Required before deploying or modifying solutions. The user can get their API key at https://mcp.ateam-ai.com/get-api-key. Read-only operations (spec, examples, validate) work without auth.",
27
+ inputSchema: {
28
+ type: "object",
29
+ properties: {
30
+ api_key: {
31
+ type: "string",
32
+ description: "Your ADAS API key (e.g., adas_xxxxx)",
33
+ },
34
+ tenant: {
35
+ type: "string",
36
+ description: "Tenant name (e.g., dev, main). Optional if your key has the format adas_<tenant>_<hex> — the tenant is auto-extracted.",
37
+ },
38
+ },
39
+ required: ["api_key"],
40
+ },
41
+ },
11
42
  {
12
43
  name: "adas_get_spec",
13
44
  description:
14
- "Get the ADAS specification — schemas, validation rules, system tools, agent guides, and templates. Use this to understand how to build skills and solutions.",
45
+ "Get the ADAS specification — schemas, validation rules, system tools, agent guides, and templates. Start here after bootstrap to understand how to build skills and solutions.",
15
46
  inputSchema: {
16
47
  type: "object",
17
48
  properties: {
@@ -25,6 +56,15 @@ export const tools = [
25
56
  required: ["topic"],
26
57
  },
27
58
  },
59
+ {
60
+ name: "adas_get_workflows",
61
+ description:
62
+ "Get the builder workflows — step-by-step state machines for building skills and solutions. Use this to guide users through the entire build process conversationally. Returns phases, what to ask, what to build, exit criteria, and tips for each stage.",
63
+ inputSchema: {
64
+ type: "object",
65
+ properties: {},
66
+ },
67
+ },
28
68
  {
29
69
  name: "adas_get_examples",
30
70
  description:
@@ -45,7 +85,7 @@ export const tools = [
45
85
  {
46
86
  name: "adas_validate_skill",
47
87
  description:
48
- "Validate a skill definition through the 5-stage ADAS validation pipeline. Returns errors and suggestions to fix.",
88
+ "Validate a skill definition through the 5-stage ADAS validation pipeline. Returns errors and suggestions to fix. Always validate before deploying.",
49
89
  inputSchema: {
50
90
  type: "object",
51
91
  properties: {
@@ -60,7 +100,7 @@ export const tools = [
60
100
  {
61
101
  name: "adas_validate_solution",
62
102
  description:
63
- "Validate a solution definition — cross-skill contracts, grant economy, handoffs, and LLM quality scoring.",
103
+ "Validate a solution definition — cross-skill contracts, grant economy, handoffs, and LLM quality scoring. Always validate before deploying.",
64
104
  inputSchema: {
65
105
  type: "object",
66
106
  properties: {
@@ -80,7 +120,7 @@ export const tools = [
80
120
  {
81
121
  name: "adas_deploy_solution",
82
122
  description:
83
- "Deploy a complete solution to ADAS Core — identity, connectors, skills. The Skill Builder auto-generates MCP servers from tool definitions. This is the main deployment action.",
123
+ "Deploy a complete solution to ADAS Core — identity, connectors, skills. The Skill Builder auto-generates MCP servers from tool definitions. This is the main deployment action. Always validate first using adas_validate_solution. Requires authentication (call adas_auth first if not using env vars).",
84
124
  inputSchema: {
85
125
  type: "object",
86
126
  properties: {
@@ -109,7 +149,7 @@ export const tools = [
109
149
  },
110
150
  {
111
151
  name: "adas_deploy_skill",
112
- description: "Deploy a single skill into an existing solution.",
152
+ description: "Deploy a single skill into an existing solution. Requires authentication.",
113
153
  inputSchema: {
114
154
  type: "object",
115
155
  properties: {
@@ -127,7 +167,7 @@ export const tools = [
127
167
  },
128
168
  {
129
169
  name: "adas_deploy_connector",
130
- description: "Deploy a connector — registers in the Skill Builder catalog and connects in ADAS Core.",
170
+ description: "Deploy a connector — registers in the Skill Builder catalog and connects in ADAS Core. Requires authentication.",
131
171
  inputSchema: {
132
172
  type: "object",
133
173
  properties: {
@@ -175,7 +215,7 @@ export const tools = [
175
215
  {
176
216
  name: "adas_update",
177
217
  description:
178
- "Update a deployed solution or skill incrementally using PATCH. Supports dot notation for scalar fields and _push/_delete/_update for arrays.",
218
+ "Update a deployed solution or skill incrementally using PATCH. Supports dot notation for scalar fields and _push/_delete/_update for arrays. Requires authentication.",
179
219
  inputSchema: {
180
220
  type: "object",
181
221
  properties: {
@@ -204,7 +244,7 @@ export const tools = [
204
244
  {
205
245
  name: "adas_redeploy",
206
246
  description:
207
- "Re-deploy after making updates. Regenerates MCP servers and pushes to ADAS Core.",
247
+ "Re-deploy after making updates. Regenerates MCP servers and pushes to ADAS Core. Requires authentication.",
208
248
  inputSchema: {
209
249
  type: "object",
210
250
  properties: {
@@ -258,30 +298,103 @@ const EXAMPLE_PATHS = {
258
298
  solution: "/spec/examples/solution",
259
299
  };
260
300
 
301
+ // Tools that require authentication (write operations)
302
+ const WRITE_TOOLS = new Set([
303
+ "adas_deploy_solution",
304
+ "adas_deploy_skill",
305
+ "adas_deploy_connector",
306
+ "adas_update",
307
+ "adas_redeploy",
308
+ "adas_solution_chat",
309
+ ]);
310
+
261
311
  const handlers = {
262
- adas_get_spec: async ({ topic }) => get(SPEC_PATHS[topic]),
312
+ adas_bootstrap: async () => ({
313
+ product_name: "A-Team (ADAS)",
314
+ what_this_mcp_is:
315
+ "A-Team MCP is a collection of tools that lets an AI assistant generate, validate, deploy, and iterate multi-agent Teams on the A-Team/ADAS platform.",
316
+ core_concepts: [
317
+ { term: "Skill", meaning: "One agent: role, intents, tools, policies, workflows" },
318
+ { term: "Solution", meaning: "A Team: multiple skills + routing + grants + handoffs" },
319
+ { term: "Connector", meaning: "External system integration via MCP tools" },
320
+ { term: "Deploy", meaning: "Make the Team runnable on ADAS Core" },
321
+ ],
322
+ recommended_flow: [
323
+ { step: 1, title: "Clarify the goal", description: "Understand what the user wants their Team to do", suggested_tools: [] },
324
+ { step: 2, title: "Generate Team map", description: "Design skills, solution architecture, and connectors", suggested_tools: ["adas_get_spec", "adas_get_examples", "adas_get_workflows"] },
325
+ { step: 3, title: "Validate", description: "Run validation before deploying", suggested_tools: ["adas_validate_skill", "adas_validate_solution"] },
326
+ { step: 4, title: "Deploy", description: "Push the Team to ADAS Core", suggested_tools: ["adas_auth", "adas_deploy_solution"] },
327
+ { step: 5, title: "Iterate", description: "Inspect, update, and redeploy as needed", suggested_tools: ["adas_get_solution", "adas_update", "adas_redeploy", "adas_solution_chat"] },
328
+ ],
329
+ first_questions: [
330
+ { id: "goal", question: "What do you want your Team to accomplish?", type: "text" },
331
+ { id: "domain", question: "Which domain fits best?", type: "enum", options: ["ecommerce", "logistics", "enterprise_ops", "other"] },
332
+ { id: "systems", question: "Which systems should the Team connect to?", type: "multi_select", options: ["slack", "email", "zendesk", "shopify", "jira", "postgres", "custom_api", "none"] },
333
+ { id: "security", question: "What environment constraints?", type: "enum", options: ["sandbox", "controlled", "regulated"] },
334
+ ],
335
+ assistant_instructions: {
336
+ always: [
337
+ "Explain Skill vs Solution vs Connector before deploying anything",
338
+ "Validate before deploy",
339
+ "Ask discovery questions if goal unclear",
340
+ ],
341
+ never: [
342
+ "Deploy before validation",
343
+ "Dump raw spec unless requested",
344
+ ],
345
+ },
346
+ }),
263
347
 
264
- adas_get_examples: async ({ type }) => get(EXAMPLE_PATHS[type]),
348
+ adas_auth: async ({ api_key, tenant }, sessionId) => {
349
+ // Auto-extract tenant from key if not provided
350
+ let resolvedTenant = tenant;
351
+ if (!resolvedTenant) {
352
+ const parsed = parseApiKey(api_key);
353
+ resolvedTenant = parsed.tenant || "main";
354
+ }
355
+ setSessionCredentials(sessionId, { tenant: resolvedTenant, apiKey: api_key });
356
+ // Verify the key works by listing solutions
357
+ try {
358
+ const result = await get("/deploy/solutions", sessionId);
359
+ return {
360
+ ok: true,
361
+ tenant: resolvedTenant,
362
+ message: `Authenticated to tenant "${resolvedTenant}". ${result.solutions?.length || 0} solution(s) found.`,
363
+ };
364
+ } catch (err) {
365
+ return {
366
+ ok: false,
367
+ tenant: resolvedTenant,
368
+ message: `Authentication failed: ${err.message}. The user can get a valid API key at https://mcp.ateam-ai.com/get-api-key`,
369
+ };
370
+ }
371
+ },
372
+
373
+ adas_get_spec: async ({ topic }, sid) => get(SPEC_PATHS[topic], sid),
374
+
375
+ adas_get_workflows: async (_args, sid) => get("/spec/workflows", sid),
376
+
377
+ adas_get_examples: async ({ type }, sid) => get(EXAMPLE_PATHS[type], sid),
265
378
 
266
- adas_validate_skill: async ({ skill }) => post("/validate/skill", { skill }),
379
+ adas_validate_skill: async ({ skill }, sid) => post("/validate/skill", { skill }, sid),
267
380
 
268
- adas_validate_solution: async ({ solution, skills }) =>
269
- post("/validate/solution", { solution, skills }),
381
+ adas_validate_solution: async ({ solution, skills }, sid) =>
382
+ post("/validate/solution", { solution, skills }, sid),
270
383
 
271
- adas_deploy_solution: async ({ solution, skills, connectors, mcp_store }) =>
272
- post("/deploy/solution", { solution, skills, connectors, mcp_store }),
384
+ adas_deploy_solution: async ({ solution, skills, connectors, mcp_store }, sid) =>
385
+ post("/deploy/solution", { solution, skills, connectors, mcp_store }, sid),
273
386
 
274
- adas_deploy_skill: async ({ solution_id, skill }) =>
275
- post(`/deploy/solutions/${solution_id}/skills`, { skill }),
387
+ adas_deploy_skill: async ({ solution_id, skill }, sid) =>
388
+ post(`/deploy/solutions/${solution_id}/skills`, { skill }, sid),
276
389
 
277
- adas_deploy_connector: async ({ connector }) =>
278
- post("/deploy/connector", { connector }),
390
+ adas_deploy_connector: async ({ connector }, sid) =>
391
+ post("/deploy/connector", { connector }, sid),
279
392
 
280
- adas_list_solutions: async () => get("/deploy/solutions"),
393
+ adas_list_solutions: async (_args, sid) => get("/deploy/solutions", sid),
281
394
 
282
- adas_get_solution: async ({ solution_id, view, skill_id }) => {
395
+ adas_get_solution: async ({ solution_id, view, skill_id }, sid) => {
283
396
  const base = `/deploy/solutions/${solution_id}`;
284
- if (skill_id) return get(`${base}/skills/${skill_id}`);
397
+ if (skill_id) return get(`${base}/skills/${skill_id}`, sid);
285
398
  const paths = {
286
399
  definition: `${base}/definition`,
287
400
  skills: `${base}/skills`,
@@ -291,30 +404,88 @@ const handlers = {
291
404
  validate: `${base}/validate`,
292
405
  connectors_health: `${base}/connectors/health`,
293
406
  };
294
- return get(paths[view]);
407
+ return get(paths[view], sid);
295
408
  },
296
409
 
297
- adas_update: async ({ solution_id, target, skill_id, updates }) => {
410
+ adas_update: async ({ solution_id, target, skill_id, updates }, sid) => {
298
411
  if (target === "skill") {
299
- return patch(`/deploy/solutions/${solution_id}/skills/${skill_id}`, { updates });
412
+ return patch(`/deploy/solutions/${solution_id}/skills/${skill_id}`, { updates }, sid);
300
413
  }
301
- return patch(`/deploy/solutions/${solution_id}`, { state_update: updates });
414
+ return patch(`/deploy/solutions/${solution_id}`, { state_update: updates }, sid);
302
415
  },
303
416
 
304
- adas_redeploy: async ({ solution_id, skill_id }) => {
417
+ adas_redeploy: async ({ solution_id, skill_id }, sid) => {
305
418
  if (skill_id) {
306
- return post(`/deploy/solutions/${solution_id}/skills/${skill_id}/redeploy`, {});
419
+ return post(`/deploy/solutions/${solution_id}/skills/${skill_id}/redeploy`, {}, sid);
307
420
  }
308
- return post(`/deploy/solutions/${solution_id}/redeploy`, {});
421
+ return post(`/deploy/solutions/${solution_id}/redeploy`, {}, sid);
309
422
  },
310
423
 
311
- adas_solution_chat: async ({ solution_id, message }) =>
312
- post(`/deploy/solutions/${solution_id}/chat`, { message }),
424
+ adas_solution_chat: async ({ solution_id, message }, sid) =>
425
+ post(`/deploy/solutions/${solution_id}/chat`, { message }, sid),
313
426
  };
314
427
 
428
+ // ─── Response formatting ────────────────────────────────────────────
429
+
430
+ // Max characters to send back in a single tool response.
431
+ // Larger payloads get summarized to avoid overwhelming LLM context.
432
+ const MAX_RESPONSE_CHARS = 50_000;
433
+
434
+ /**
435
+ * Format tool results — summarize oversized payloads.
436
+ */
437
+ function formatResult(result, toolName) {
438
+ const json = JSON.stringify(result, null, 2);
439
+
440
+ if (json.length <= MAX_RESPONSE_CHARS) {
441
+ return json;
442
+ }
443
+
444
+ // For large responses, provide a summary + truncated data
445
+ const summary = summarizeLargeResult(result, toolName);
446
+ return summary + `\n\n(Response truncated from ${json.length.toLocaleString()} chars. Use more specific queries to get smaller results.)`;
447
+ }
448
+
449
+ /**
450
+ * Create a useful summary for large results.
451
+ */
452
+ function summarizeLargeResult(result, toolName) {
453
+ // Spec responses — keep content but cap size
454
+ if (toolName === "adas_get_spec" && result && typeof result === "object") {
455
+ const keys = Object.keys(result);
456
+ return JSON.stringify({
457
+ _note: `ADAS spec with ${keys.length} sections. Content truncated — ask about specific sections for detail.`,
458
+ sections: keys,
459
+ ...result,
460
+ }, null, 2).slice(0, MAX_RESPONSE_CHARS);
461
+ }
462
+
463
+ // Validation results — keep errors/warnings, trim echoed input
464
+ if ((toolName === "adas_validate_skill" || toolName === "adas_validate_solution") && result) {
465
+ const slim = { ...result };
466
+ if (slim.skill) delete slim.skill;
467
+ if (slim.solution) delete slim.solution;
468
+ const slimJson = JSON.stringify(slim, null, 2);
469
+ if (slimJson.length <= MAX_RESPONSE_CHARS) return slimJson;
470
+ }
471
+
472
+ // Export results — summarize structure
473
+ if (toolName === "adas_get_solution" && result?.skills) {
474
+ return JSON.stringify({
475
+ _note: `Solution with ${result.skills.length} skill(s). Use adas_get_solution with skill_id to inspect individual skills.`,
476
+ solution_id: result.solution?.id || result.id,
477
+ skill_ids: result.skills.map(s => s.id || s.name),
478
+ ...result,
479
+ }, null, 2).slice(0, MAX_RESPONSE_CHARS);
480
+ }
481
+
482
+ // Generic fallback — truncate
483
+ return JSON.stringify(result, null, 2).slice(0, MAX_RESPONSE_CHARS);
484
+ }
485
+
315
486
  // ─── Dispatcher ─────────────────────────────────────────────────────
316
487
 
317
- export async function handleToolCall(name, args) {
488
+ export async function handleToolCall(name, args, sessionId) {
318
489
  const handler = handlers[name];
319
490
  if (!handler) {
320
491
  return {
@@ -322,14 +493,36 @@ export async function handleToolCall(name, args) {
322
493
  isError: true,
323
494
  };
324
495
  }
496
+
497
+ // Check auth for write operations
498
+ if (WRITE_TOOLS.has(name) && !isAuthenticated(sessionId)) {
499
+ return {
500
+ content: [{
501
+ type: "text",
502
+ text: [
503
+ "🔐 Authentication required.",
504
+ "",
505
+ "This tool needs an API key. Please ask the user to:",
506
+ "",
507
+ "1. Get their API key at: https://mcp.ateam-ai.com/get-api-key",
508
+ "2. Then call: adas_auth(api_key: \"<their key>\")",
509
+ "",
510
+ "The key looks like: adas_<tenant>_<32hex>",
511
+ "The tenant is auto-extracted — no separate tenant parameter needed.",
512
+ ].join("\n"),
513
+ }],
514
+ isError: true,
515
+ };
516
+ }
517
+
325
518
  try {
326
- const result = await handler(args);
519
+ const result = await handler(args, sessionId);
327
520
  return {
328
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
521
+ content: [{ type: "text", text: formatResult(result, name) }],
329
522
  };
330
523
  } catch (err) {
331
524
  return {
332
- content: [{ type: "text", text: `Error: ${err.message}` }],
525
+ content: [{ type: "text", text: err.message }],
333
526
  isError: true,
334
527
  };
335
528
  }