11import z from "zod"
2+ import { readFile } from "fs/promises"
3+ import path from "path"
24import { Tool } from "../../tool/tool"
35import { AltimateApi } from "../api/client"
46import { MCP } from "../../mcp"
@@ -11,6 +13,9 @@ import {
1113} from "../../mcp/config"
1214import { Instance } from "../../project/instance"
1315import { Global } from "../../global"
16+ import { Log } from "../../util/log"
17+
18+ const log = Log . create ( { service : "datamate" } )
1419
1520/** Project root for config resolution — falls back to cwd when no git repo is detected. */
1621function projectRoot ( ) {
@@ -25,6 +30,46 @@ export function slugify(name: string): string {
2530 . replace ( / ^ - | - $ / g, "" )
2631}
2732
33+ // altimate_change start — read transport type from .vscode/mcp.json
34+ // Returns { type: "remote", url } if the datamate entry is an HTTP server,
35+ // { type: "local" } if it is a stdio server, or null if the file is missing
36+ // or no datamate entry is found. The caller uses this to pick the right
37+ // mcpConfig shape and falls back to the cloud config when null is returned.
38+ async function readVscodeMcpTransport (
39+ projectRootDir : string ,
40+ ) : Promise < { type : "remote" ; url : string } | { type : "local" } | null > {
41+ try {
42+ const mcpJsonPath = path . join ( projectRootDir , ".vscode" , "mcp.json" )
43+ const text = await readFile ( mcpJsonPath , "utf-8" )
44+ const parsed = JSON . parse ( text ) as Record < string , unknown >
45+
46+ // .vscode/mcp.json uses either "servers" (VS Code 1.99+) or "mcpServers" key
47+ const serversMap =
48+ ( parsed [ "servers" ] as Record < string , Record < string , unknown > > | undefined ) ??
49+ ( parsed [ "mcpServers" ] as Record < string , Record < string , unknown > > | undefined ) ??
50+ { }
51+
52+ for ( const [ key , entry ] of Object . entries ( serversMap ) ) {
53+ const args = Array . isArray ( entry [ "args" ] ) ? ( entry [ "args" ] as string [ ] ) : [ ]
54+ const isDatamate =
55+ key === "datamate" ||
56+ args . some ( ( a ) => a . includes ( "start-stdio" ) || a . includes ( "datamate-cli" ) )
57+
58+ if ( ! isDatamate ) continue
59+
60+ if ( typeof entry [ "url" ] === "string" ) {
61+ return { type : "remote" , url : entry [ "url" ] }
62+ }
63+ return { type : "local" }
64+ }
65+ return null
66+ } catch {
67+ // File missing or unparseable — caller falls back to cloud config
68+ return null
69+ }
70+ }
71+ // altimate_change end
72+
2873export const DatamateManagerTool = Tool . define ( "datamate_manager" , {
2974 description :
3075 "Manage Altimate Datamates — AI teammates with integrations (Snowflake, Jira, dbt, etc). " +
@@ -39,7 +84,9 @@ export const DatamateManagerTool = Tool.define("datamate_manager", {
3984 "'list-config' shows all datamate entries saved in config files (project and global). " +
4085 "Config files: project config is at <project-root>/altimate-code.json, " +
4186 "global config is at ~/.config/altimate-code/altimate-code.json. " +
42- "Datamate server names are prefixed with 'datamate-'. " +
87+ "When a VS Code extension datamate entry exists (.vscode/mcp.json has 'datamate' key), " +
88+ "'add' always uses the server name 'datamate' — tools are then prefixed 'datamate_'. " +
89+ "In standalone mode, server names follow 'datamate-<name>' pattern. " +
4390 "Do NOT use glob/grep/read to find config files — use 'list-config' instead." ,
4491 parameters : z . object ( {
4592 operation : z . enum ( [ "list" , "list-integrations" , "add" , "create" , "edit" , "delete" , "status" , "remove" , "list-config" ] ) ,
@@ -154,6 +201,10 @@ async function handleListIntegrations() {
154201 }
155202}
156203
204+ // altimate_change start — server name used by the VS Code extension in .vscode/mcp.json
205+ const EXTENSION_DATAMATE_SERVER = "datamate"
206+ // altimate_change end
207+
157208async function handleAdd ( args : { datamate_id ?: string ; name ?: string ; scope ?: "project" | "global" } ) {
158209 if ( ! args . datamate_id ) {
159210 return {
@@ -163,17 +214,76 @@ async function handleAdd(args: { datamate_id?: string; name?: string; scope?: "p
163214 }
164215 }
165216 try {
166- const creds = await AltimateApi . getCredentials ( )
167217 const datamate = await AltimateApi . getDatamate ( args . datamate_id )
168- const serverName = args . name ?? `datamate-${ slugify ( datamate . name ) } `
169- const mcpConfig = AltimateApi . buildMcpConfig ( creds , args . datamate_id )
218+ const transport = await readVscodeMcpTransport ( projectRoot ( ) )
219+
220+ // altimate_change start — single-gateway mode when extension is present
221+ // If .vscode/mcp.json has a "datamate" entry (written by the VS Code extension),
222+ // always use "datamate" as the server name regardless of which specific datamate
223+ // the user selected. This prevents duplicate tool sets — the extension's gateway
224+ // already serves all datamate tools through a single MCP connection.
225+ // In standalone/CLI mode (no .vscode/mcp.json datamate entry), fall back to the
226+ // original per-datamate naming with cloud URL.
227+ const serverName = transport !== null
228+ ? EXTENSION_DATAMATE_SERVER
229+ : ( args . name ?? `datamate-${ slugify ( datamate . name ) } ` )
230+
231+ const creds = transport ? undefined : await AltimateApi . getCredentials ( )
232+ const mcpConfig =
233+ transport ?. type === "remote"
234+ ? { type : "remote" as const , url : transport . url }
235+ : transport ?. type === "local"
236+ // Extension stdio: no --datamate id needed — active teammate is resolved
237+ // by the extension over the ALTIMATE_EXTENSION_RPC socket at runtime.
238+ ? { type : "local" as const , command : [ "datamate" , "start-stdio" ] }
239+ : AltimateApi . buildMcpConfig ( creds ! , args . datamate_id )
170240
171- // Always save to config first so it persists for future sessions
172241 const isGlobal = args . scope === "global"
173242 const configPath = await resolveConfigPath ( isGlobal ? Global . Path . config : projectRoot ( ) , isGlobal )
174- await addMcpToConfig ( serverName , mcpConfig , configPath )
175243
176- await MCP . add ( serverName , mcpConfig )
244+ if ( transport !== null ) {
245+ // Extension mode: check if "datamate" is already wired up
246+ const existingNames = await listMcpInConfig ( configPath )
247+ const staleEntries = existingNames . filter (
248+ ( n ) => n !== EXTENSION_DATAMATE_SERVER && n . startsWith ( "datamate-" ) ,
249+ )
250+ if ( staleEntries . length > 0 ) {
251+ log . info ( "handleAdd: stale per-datamate entries detected alongside extension gateway" , {
252+ staleEntries,
253+ } )
254+ }
255+
256+ if ( existingNames . includes ( EXTENSION_DATAMATE_SERVER ) ) {
257+ // Already in config — just ensure it is connected in this session
258+ const allStatus = await MCP . status ( )
259+ if ( allStatus [ EXTENSION_DATAMATE_SERVER ] ?. status === "connected" ) {
260+ const mcpTools = await MCP . tools ( )
261+ const toolCount = Object . keys ( mcpTools ) . filter ( ( k ) =>
262+ k . startsWith ( EXTENSION_DATAMATE_SERVER + "_" ) ,
263+ ) . length
264+ const staleNote =
265+ staleEntries . length > 0
266+ ? `\n\nNote: stale per-datamate entries found in config: ${ staleEntries . join ( ", " ) } — use operation 'remove' to clean them up.`
267+ : ""
268+ return {
269+ title : `Datamate '${ datamate . name } ': already connected via '${ EXTENSION_DATAMATE_SERVER } '` ,
270+ metadata : { serverName : EXTENSION_DATAMATE_SERVER , datamateId : args . datamate_id , toolCount } ,
271+ output : `Datamate tools are already available via the '${ EXTENSION_DATAMATE_SERVER } ' MCP server (${ toolCount } tools active).${ staleNote } ` ,
272+ }
273+ }
274+ // In config but not connected — reconnect
275+ await MCP . add ( EXTENSION_DATAMATE_SERVER , mcpConfig )
276+ } else {
277+ // Not in config yet — write then connect
278+ await addMcpToConfig ( EXTENSION_DATAMATE_SERVER , { ...mcpConfig , enabled : true } , configPath )
279+ await MCP . add ( EXTENSION_DATAMATE_SERVER , mcpConfig )
280+ }
281+ } else {
282+ // Standalone/CLI mode — original behaviour: per-datamate name + cloud URL
283+ await addMcpToConfig ( serverName , { ...mcpConfig , enabled : true } , configPath )
284+ await MCP . add ( serverName , mcpConfig )
285+ }
286+ // altimate_change end
177287
178288 // Check connection status
179289 const allStatus = await MCP . status ( )
@@ -197,7 +307,7 @@ async function handleAdd(args: { datamate_id?: string; name?: string; scope?: "p
197307 return {
198308 title : `Datamate '${ datamate . name } ': connected as '${ serverName } '` ,
199309 metadata : { serverName, datamateId : args . datamate_id , toolCount, configPath } ,
200- output : `Connected datamate '${ datamate . name } ' (ID: ${ args . datamate_id } ) as MCP server '${ serverName } '.\n\n${ toolCount } tools are now available from this datamate . They will be usable in the next message.\n\nConfiguration saved to ${ configPath } for future sessions.` ,
310+ output : `Connected datamate '${ datamate . name } ' (ID: ${ args . datamate_id } ) as MCP server '${ serverName } '.\n\n${ toolCount } tools are now available. They will be usable in the next message.\n\nConfiguration saved to ${ configPath } for future sessions.` ,
201311 }
202312 } catch ( e ) {
203313 return {
0 commit comments