Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions docs/mcp-workspaces.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Workspaces for external coding clients (experimental)

`altimate-code mcp serve --workspace-tools` adds workspace context, named skills and known read operations from the linked workspace's existing `datamate` engine. The default MCP server remains unchanged. This draft builds on the experimental MCP server; no rollout or merge is implied.

The client must run in the intended project directory. Sign-in and linking may complete after connecting: `workspace_context` returns structured setup status and the catalog remains stable. `workspace_context` returns the current workspace, bounded project memory, a summary of skills/knowledge/integrations, and skill names. `workspace_skill` reads a named skill from the validated snapshot; refresh missing skills with `altimate-code workspace refresh`. It rejects traversal, symlinks, oversized skills and a changed snapshot/account.

The engine is pinned with its existing `--datamate` option. Only knowledge, supported metadata reads and SQL reads are exposed; arbitrary tools, writes and engine memory mutations are excluded. SQL uses the existing classifier. Unsupported SQL argument schemas are withheld. The engine authenticates through its normal local configuration. Use read-only warehouse credentials: classification is not database isolation. Local SQL remains available if workspace authentication, linking or engine startup fails. An account/binding change invalidates stale receipts. Read fresh context and use the new receipt; reconnecting MCP is not required.

For non-model clients:

```
altimate-code workspace-client list
altimate-code workspace-client context --directory /absolute/project
printf '%s' 'Use booked ARR for recurring revenue.' | altimate-code workspace-client remember --directory /absolute/project --token=<context-workspace-token> --id=metrics/arr
```

These commands output JSON. A context receipt binds account, workspace and project. `remember` requires that receipt, a valid memory id and 1–2,048 characters on stdin; it saves exactly one project block. It suppresses the ordinary fire-and-forget mirror, then uses the existing sync path with the explicitly selected binding. Requests pin credentials across async API/index operations. A backend failure after local persistence reports `savedLocally: true, cloudConfirmed: false`; retrying is a user decision. Confirmed cloud publication is reported only when the sync reports one successful write. No model-callable memory-write tool or bulk-sync operation is added.

Workspace context adds bounded data to a client's prompt; skills load on demand. Explicit knowledge calls can use backend models, memory publication can invoke the existing backend extractor, and warehouse calls retain warehouse costs. There is no new reasoning agent or per-row model loop. A CLI link can normally backfill existing memory; clients that expose linking without a backfill confirmation must disable memory for that subprocess, as the Claude plugin does. Do not pass `--yes` unless replacing a link was explicitly requested.

Verification covers unit contracts, a real stdio protocol fixture and existing SQLite MCP behavior. Production read-only testing listed visible workspaces and loaded matching context through a temporary local project. Production bindings and memory were not written. A non-empty cloud memory round trip and real workspace-engine SQL/knowledge calls remain rollout checks.

## Automatic-client setup and stable gateway

The opt-in workspace catalog is always `workspace_context`, `workspace_skill` and `workspace_execute`, including when initially unlinked. `workspace_context` reports `needs_auth`, `needs_workspace`, `disabled` or `unavailable` without returning raw credential errors. A ready context includes the workspace receipt. Context loads coalesce and cache for 30 seconds; identity is verified on every use. `refresh: true` bypasses that cache.

Pass `include_tools: true` to request lazy engine discovery. The response includes `engineStatus` (`ready`, `needs_engine` or `unavailable`) and an operation catalog with the engine's actual input schemas. After explicit engine installation, request both `refresh: true` and `include_tools: true`. `workspace_execute` takes `workspace_token`, `operation` and an `arguments` object matching the returned schema. Unknown operations, invalid schemas, stale receipts and source-write SQL are refused. `workspace_skill` also requires `workspace_token`.

This gateway intentionally changes the host permission surface: allowing `workspace_execute` permits its entire closed read-operation allowlist, including knowledge requests that can invoke backend models. It does not inherit per-operation approvals from the old dynamically discovered tool names. No workspace memory write is exposed as an MCP tool.

`workspace-client context` returns the same structured setup status. `workspace-client engine-install` invokes the existing native installer; a UI must require an explicit user action before invoking it. The Claude pilot runs native sign-in and link actions from its first-use panel. Linking disables memory backfill for that subprocess. Authentication output stays outside model context. Default MCP startup without `--workspace-tools` is unchanged.
4 changes: 4 additions & 0 deletions packages/opencode/src/altimate/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import path from "path"
import { rm } from "node:fs/promises"
import { Global } from "../../global"
import { Filesystem } from "../../util/filesystem"
import { currentCredentials } from "./credentials-context"

const DEFAULT_MCP_URL = "https://mcpserver.getaltimate.com/sse"
// altimate_change start — default Altimate API URL when user omits it from the TUI credential entry
Expand Down Expand Up @@ -67,6 +68,7 @@ export namespace AltimateApi {
}

export async function isConfigured(): Promise<boolean> {
if (currentCredentials()) return true
return Filesystem.exists(credentialsPath())
}

Expand All @@ -85,6 +87,8 @@ export namespace AltimateApi {
}

export async function getCredentials(): Promise<AltimateCredentials> {
const scoped = currentCredentials()
if (scoped) return scoped
const p = credentialsPath()
if (!(await Filesystem.exists(p))) {
throw new Error(`Altimate credentials not found at ${p}`)
Expand Down
19 changes: 19 additions & 0 deletions packages/opencode/src/altimate/api/credentials-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { AsyncLocalStorage } from "node:async_hooks"

export interface Credentials {
altimateUrl: string
altimateInstanceName: string
altimateApiKey: string
mcpServerUrl?: string
}

const credentials = new AsyncLocalStorage<Credentials>()

/** Pin every API/index operation in a bounded external-client request to one account. */
export function withCredentials<T>(snapshot: Credentials, run: () => Promise<T>): Promise<T> {
return credentials.run(Object.freeze({ ...snapshot }), run)
}

export function currentCredentials(): Credentials | undefined {
return credentials.getStore()
}
11 changes: 8 additions & 3 deletions packages/opencode/src/altimate/mcp-server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export interface ServeOptions {
allowWrite?: boolean
// Also advertise schema_inspect and sql_check (default: the five-tool set, see adapter LEAN_TOOLS).
allTools?: boolean
workspaceTools?: boolean
// Bounds the reading question so an unanswered prompt cannot outlast the client's tool-call timeout.
elicitTimeoutMs?: number
}
Expand All @@ -83,16 +84,17 @@ export async function createServer(options: ServeOptions = {}) {
}
}
const adapter = await createAdapter(options)
const workspace = options.workspaceTools ? await (await import("./workspace")).createWorkspaceAdapter(process.cwd()) : undefined
const server = new Server({ name: "altimate-code", version: InstallationVersion }, {
capabilities: { tools: {}, prompts: {} },
instructions: await serverInstructions(),
})
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [...adapter.tools].map(([name, tool]) => ({
tools: [...[...adapter.tools].map(([name, tool]) => ({
name,
description: descriptions[name],
inputSchema: tool.jsonSchema as McpTool["inputSchema"],
})),
})), ...(workspace?.tools ?? [])],
}))
// Ask the user once per session when the model weighed two readings and the client can ask; any
// failure, decline or cancel (headless clients cancel) leaves the commit as accepted.
Expand Down Expand Up @@ -131,6 +133,9 @@ export async function createServer(options: ServeOptions = {}) {
})
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
try {
if (workspace?.tools.some((tool) => tool.name === request.params.name)) {
return await workspace.execute(request.params.name, request.params.arguments ?? {}, extra.signal)
}
const result = await adapter.execute(request.params.name, request.params.arguments ?? {}, extra.signal)
const clarified = request.params.name === "answer_commit" ? await clarifyReading(request.params.arguments, result, extra.signal) : undefined
if (clarified) return { content: [{ type: "text" as const, text: clarified }], isError: true }
Expand All @@ -147,7 +152,7 @@ export async function createServer(options: ServeOptions = {}) {
}
})
let cleanup: Promise<void> | undefined
const dispose = () => (cleanup ??= adapter.close())
const dispose = () => (cleanup ??= Promise.all([adapter.close(), workspace?.close()]).then(() => {}))
server.onclose = async () => {
await dispose().catch(console.error)
}
Expand Down
Loading
Loading