Skip to content

Commit 9a02f27

Browse files
authored
Merge pull request #99 from AltimateAI/feat/datamate-manager-clean
feat: datamate manager — dynamic MCP server management
2 parents 4ae73c0 + d89d458 commit 9a02f27

File tree

10 files changed

+973
-62
lines changed

10 files changed

+973
-62
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
name: altimate-setup
3+
description: Configure Altimate platform credentials for datamate and API access
4+
---
5+
6+
# Altimate Setup
7+
8+
Guide the user through configuring their Altimate platform credentials.
9+
10+
## Steps
11+
12+
1. **Check existing config**: Read `~/.altimate/altimate.json`. If it exists and is valid, show the current config (mask the API key) and ask if they want to update it.
13+
14+
2. **Gather credentials**: Ask the user for:
15+
- **Altimate URL** (default: `https://api.myaltimate.com`)
16+
- **Instance name** (their tenant/org name, e.g. `megatenant`)
17+
- **API key** (from Altimate platform settings)
18+
- **MCP server URL** (optional, default: `https://mcpserver.getaltimate.com/sse`)
19+
20+
3. **Write config**: Create `~/.altimate/` directory if needed, then write `~/.altimate/altimate.json`:
21+
```json
22+
{
23+
"altimateUrl": "<url>",
24+
"altimateInstanceName": "<instance>",
25+
"altimateApiKey": "<key>",
26+
"mcpServerUrl": "<mcp-url>"
27+
}
28+
```
29+
Then set permissions to owner-only: `chmod 600 ~/.altimate/altimate.json`
30+
31+
4. **Validate**: Call the `datamate_manager` tool with `operation: "list"` to verify the credentials work. Report success or failure to the user.
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import z from "zod"
2+
import path from "path"
3+
import { Global } from "../../global"
4+
import { Filesystem } from "../../util/filesystem"
5+
6+
const DEFAULT_MCP_URL = "https://mcpserver.getaltimate.com/sse"
7+
8+
const AltimateCredentials = z.object({
9+
altimateUrl: z.string(),
10+
altimateInstanceName: z.string(),
11+
altimateApiKey: z.string(),
12+
mcpServerUrl: z.string().optional(),
13+
})
14+
type AltimateCredentials = z.infer<typeof AltimateCredentials>
15+
16+
const DatamateSummary = z.object({
17+
id: z.coerce.string(),
18+
name: z.string(),
19+
description: z.string().nullable().optional(),
20+
integrations: z
21+
.array(
22+
z.object({
23+
id: z.coerce.string(),
24+
tools: z.array(z.object({ key: z.string() })).optional(),
25+
}),
26+
)
27+
.nullable()
28+
.optional(),
29+
memory_enabled: z.boolean().optional(),
30+
privacy: z.string().optional(),
31+
})
32+
33+
const IntegrationSummary = z.object({
34+
id: z.coerce.string(),
35+
name: z.string().optional(),
36+
description: z.string().nullable().optional(),
37+
tools: z
38+
.array(
39+
z.object({
40+
key: z.string(),
41+
name: z.string().optional(),
42+
enable_all: z.array(z.string()).optional(),
43+
}),
44+
)
45+
.optional(),
46+
})
47+
48+
export namespace AltimateApi {
49+
export function credentialsPath(): string {
50+
return path.join(Global.Path.home, ".altimate", "altimate.json")
51+
}
52+
53+
export async function isConfigured(): Promise<boolean> {
54+
return Filesystem.exists(credentialsPath())
55+
}
56+
57+
export async function getCredentials(): Promise<AltimateCredentials> {
58+
const p = credentialsPath()
59+
if (!(await Filesystem.exists(p))) {
60+
throw new Error(`Altimate credentials not found at ${p}`)
61+
}
62+
const raw = JSON.parse(await Filesystem.readText(p))
63+
return AltimateCredentials.parse(raw)
64+
}
65+
66+
async function request(creds: AltimateCredentials, method: string, endpoint: string, body?: unknown) {
67+
const url = `${creds.altimateUrl}${endpoint}`
68+
const res = await fetch(url, {
69+
method,
70+
headers: {
71+
"Content-Type": "application/json",
72+
Authorization: `Bearer ${creds.altimateApiKey}`,
73+
"x-tenant": creds.altimateInstanceName,
74+
},
75+
...(body ? { body: JSON.stringify(body) } : {}),
76+
})
77+
if (!res.ok) {
78+
throw new Error(`API ${method} ${endpoint} failed with status ${res.status}`)
79+
}
80+
return res.json()
81+
}
82+
83+
export async function listDatamates() {
84+
const creds = await getCredentials()
85+
const data = await request(creds, "GET", "/datamates/")
86+
const list = Array.isArray(data) ? data : (data.datamates ?? data.data ?? [])
87+
return list.map((d: unknown) => DatamateSummary.parse(d)) as z.infer<typeof DatamateSummary>[]
88+
}
89+
90+
export async function getDatamate(id: string) {
91+
const creds = await getCredentials()
92+
try {
93+
const data = await request(creds, "GET", `/datamates/${id}/summary`)
94+
const raw = data.datamate ?? data
95+
return DatamateSummary.parse(raw)
96+
} catch (e) {
97+
// Fallback to list if single-item endpoint is unavailable (404)
98+
if (e instanceof Error && e.message.includes("status 404")) {
99+
const all = await listDatamates()
100+
const found = all.find((d) => d.id === id)
101+
if (!found) {
102+
throw new Error(`Datamate with ID ${id} not found`)
103+
}
104+
return found
105+
}
106+
throw e
107+
}
108+
}
109+
110+
export async function createDatamate(payload: {
111+
name: string
112+
description?: string
113+
integrations?: Array<{ id: string; tools: Array<{ key: string }> }>
114+
memory_enabled?: boolean
115+
privacy?: string
116+
}) {
117+
const creds = await getCredentials()
118+
const data = await request(creds, "POST", "/datamates/", payload)
119+
// Backend returns { id: number } for create
120+
const id = String(data.id ?? data.datamate?.id)
121+
return { id, name: payload.name }
122+
}
123+
124+
export async function updateDatamate(
125+
id: string,
126+
payload: {
127+
name?: string
128+
description?: string
129+
integrations?: Array<{ id: string; tools: Array<{ key: string }> }>
130+
memory_enabled?: boolean
131+
privacy?: string
132+
},
133+
) {
134+
const creds = await getCredentials()
135+
const data = await request(creds, "PATCH", `/datamates/${id}`, payload)
136+
const raw = data.datamate ?? data
137+
return DatamateSummary.parse(raw)
138+
}
139+
140+
export async function deleteDatamate(id: string) {
141+
const creds = await getCredentials()
142+
await request(creds, "DELETE", `/datamates/${id}`)
143+
}
144+
145+
export async function listIntegrations() {
146+
const creds = await getCredentials()
147+
const data = await request(creds, "GET", "/datamate_integrations/")
148+
const list = Array.isArray(data) ? data : (data.integrations ?? data.data ?? [])
149+
return list.map((d: unknown) => IntegrationSummary.parse(d)) as z.infer<typeof IntegrationSummary>[]
150+
}
151+
152+
/** Resolve integration IDs to full integration objects with all tools enabled (matching frontend behavior). */
153+
export async function resolveIntegrations(
154+
integrationIds: string[],
155+
): Promise<Array<{ id: string; tools: Array<{ key: string }> }>> {
156+
const allIntegrations = await listIntegrations()
157+
return integrationIds.map((id) => {
158+
const def = allIntegrations.find((i) => i.id === id)
159+
const tools =
160+
def?.tools?.flatMap((t) => (t.enable_all ?? [t.key]).map((k) => ({ key: k }))) ?? []
161+
return { id, tools }
162+
})
163+
}
164+
165+
export function buildMcpConfig(creds: AltimateCredentials, datamateId: string) {
166+
return {
167+
type: "remote" as const,
168+
url: creds.mcpServerUrl ?? DEFAULT_MCP_URL,
169+
oauth: false as const,
170+
headers: {
171+
Authorization: `Bearer ${creds.altimateApiKey}`,
172+
"x-datamate-id": String(datamateId),
173+
"x-tenant": creds.altimateInstanceName,
174+
"x-altimate-url": creds.altimateUrl,
175+
},
176+
}
177+
}
178+
}

0 commit comments

Comments
 (0)