Skip to content

Commit 14f5cbd

Browse files
feat: [AI-7392] authoritative tool source + humanized MCP titles
Classify every tool call's origin server-side and stamp it on the tool part's state.metadata.source (builtin | altimate | mcp), so clients render the source badge without re-deriving it from tool-name prefixes: - tool-source.ts: native-set inversion (any non-native registry tool is Altimate, so new Altimate tools classify with no maintenance); Datamates MCP folded into "altimate"; humanizeMcpTitle for readable MCP labels. - resolveTools: stamp metadata.source on registry tools; on MCP tools set both the source and a humanized title (they previously had title: "").
1 parent 9e1853e commit 14f5cbd

3 files changed

Lines changed: 119 additions & 1 deletion

File tree

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Authoritative classification of a tool call's origin, stamped onto the tool
3+
* part's `state.metadata.source` so clients (chat webview, ...) render the right
4+
* badge without re-deriving it from tool-name prefixes.
5+
*
6+
* - "builtin" — native opencode tools (read/glob/bash/...)
7+
* - "altimate" — Altimate-provided tools (sql_*, schema_*, finops_*, ...) AND
8+
* tools from the Datamates MCP server (Altimate-owned, just
9+
* delivered over MCP)
10+
* - "mcp" — third-party MCP tools
11+
*
12+
* Registry tools and MCP tools are resolved in separate loops (see
13+
* `session/prompt.ts` resolveTools), so each has its own classifier.
14+
*/
15+
export type ToolSource = "builtin" | "altimate" | "mcp"
16+
17+
/**
18+
* Native opencode tool ids. This set is small and stable; every other tool in
19+
* the registry is Altimate-provided, so new Altimate tools classify correctly
20+
* with no per-tool maintenance here.
21+
*/
22+
const NATIVE_TOOL_IDS = new Set<string>([
23+
"invalid",
24+
"question",
25+
"bash",
26+
"read",
27+
"glob",
28+
"grep",
29+
"list",
30+
"edit",
31+
"write",
32+
"multiedit",
33+
"task",
34+
"webfetch",
35+
"todowrite",
36+
"todoread",
37+
"websearch",
38+
"codesearch",
39+
"skill",
40+
"apply_patch",
41+
"lsp",
42+
"plan_exit",
43+
"plan_enter",
44+
"StructuredOutput",
45+
])
46+
47+
/** MCP client-name prefixes that are Altimate-owned (Datamates as an MCP server). */
48+
const ALTIMATE_MCP_PREFIXES = ["datamate"]
49+
50+
/** Classify a registry tool (never an MCP tool) as builtin vs Altimate. */
51+
export function registryToolSource(id: string): ToolSource {
52+
return NATIVE_TOOL_IDS.has(id) ? "builtin" : "altimate"
53+
}
54+
55+
/** Classify an MCP tool by its `<client>_<tool>` key: Altimate (Datamates) vs third-party. */
56+
export function mcpToolSource(key: string): ToolSource {
57+
const lower = key.toLowerCase()
58+
return ALTIMATE_MCP_PREFIXES.some((p) => lower.startsWith(p)) ? "altimate" : "mcp"
59+
}
60+
61+
/**
62+
* Best-effort readable title for an MCP tool call, from its `<client>_<tool>`
63+
* key — e.g. "datamates_jira_get_issue" → "Jira Get Issue". Strips the leading
64+
* client segment and Title-Cases the rest. (Richer per-call titles are the MCP
65+
* server's job; this is the fallback so MCP rows aren't a bare snake_case id.)
66+
*/
67+
export function humanizeMcpTitle(key: string): string {
68+
const withoutClient = key.includes("_") ? key.slice(key.indexOf("_") + 1) : key
69+
const words = (withoutClient || key).split(/[_-]+/).filter(Boolean)
70+
if (words.length === 0) return key
71+
return words.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ")
72+
}

packages/opencode/src/session/prompt.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ import { registerAltimateValidators } from "../altimate/validators"
6565
registerAltimateValidators()
6666
import { Config } from "../config/config"
6767
import { Tracer } from "../altimate/observability/tracing"
68+
// altimate_change — stamp an authoritative tool source + humanized MCP title
69+
import { registryToolSource, mcpToolSource, humanizeMcpTitle } from "../altimate/tool-source"
6870
// altimate_change end
6971
import { Telemetry } from "@/telemetry" // altimate_change — session telemetry
7072

@@ -1564,6 +1566,8 @@ export namespace SessionPrompt {
15641566
messageID: input.processor.message.id,
15651567
})),
15661568
}
1569+
// altimate_change — stamp authoritative tool source so clients render the right badge
1570+
output.metadata = { ...(output.metadata ?? {}), source: registryToolSource(item.id) }
15671571
await Plugin.trigger(
15681572
"tool.execute.after",
15691573
{
@@ -1655,10 +1659,13 @@ export namespace SessionPrompt {
16551659
...(result.metadata ?? {}),
16561660
truncated: truncated.truncated,
16571661
...(truncated.truncated && { outputPath: truncated.outputPath }),
1662+
// altimate_change — authoritative source so the chat can badge Datamates MCP tools
1663+
source: mcpToolSource(key),
16581664
}
16591665

16601666
return {
1661-
title: "",
1667+
// altimate_change — MCP tools have no native title; give a readable label
1668+
title: humanizeMcpTitle(key),
16621669
metadata,
16631670
output: truncated.content,
16641671
attachments: attachments.map((attachment) => ({
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { describe, test, expect } from "bun:test"
2+
import { registryToolSource, mcpToolSource, humanizeMcpTitle } from "../../src/altimate/tool-source"
3+
4+
describe("registryToolSource", () => {
5+
test("native opencode tools → builtin", () => {
6+
for (const id of ["read", "write", "edit", "glob", "grep", "list", "bash", "task", "skill", "apply_patch"]) {
7+
expect(registryToolSource(id)).toBe("builtin")
8+
}
9+
})
10+
11+
test("any non-native registry tool → altimate (incl. tools not enumerated here)", () => {
12+
for (const id of ["sql_analyze", "schema_inspect", "finops_query_history", "altimate_core_check", "data_diff", "some_new_altimate_tool"]) {
13+
expect(registryToolSource(id)).toBe("altimate")
14+
}
15+
})
16+
})
17+
18+
describe("mcpToolSource", () => {
19+
test("Datamates MCP tools → altimate", () => {
20+
expect(mcpToolSource("datamates_jira_get_issue")).toBe("altimate")
21+
expect(mcpToolSource("datamate_snowflake_query")).toBe("altimate")
22+
})
23+
24+
test("third-party MCP tools → mcp", () => {
25+
expect(mcpToolSource("github_search_issues")).toBe("mcp")
26+
expect(mcpToolSource("linear_create_issue")).toBe("mcp")
27+
})
28+
})
29+
30+
describe("humanizeMcpTitle", () => {
31+
test("strips the client segment and Title-Cases the rest", () => {
32+
expect(humanizeMcpTitle("datamates_jira_get_issue")).toBe("Jira Get Issue")
33+
expect(humanizeMcpTitle("github_search_issues")).toBe("Search Issues")
34+
})
35+
36+
test("falls back gracefully for single-segment keys", () => {
37+
expect(humanizeMcpTitle("ping")).toBe("Ping")
38+
})
39+
})

0 commit comments

Comments
 (0)