@actioneer/ads-mcp 0.1.0
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/README.md +79 -0
- package/bin/ads-mcp.mjs +46 -0
- package/mcp.example.json +8 -0
- package/package.json +41 -0
- package/src/lint.mjs +42 -0
- package/src/server.mjs +66 -0
- package/src/tools.mjs +168 -0
package/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# `@actioneer/ads-mcp` — the ADS MCP server
|
|
2
|
+
|
|
3
|
+
Lets a coding agent in **Claude Code / Cursor** discover and import the Actioneer
|
|
4
|
+
Design System through the same flow it already uses for shadcn — always pointing
|
|
5
|
+
at the canonical `@actioneer/ads` package, **never a fork or a deep import**.
|
|
6
|
+
|
|
7
|
+
It is a thin MCP layer over two pure, unit-tested cores, both declared as **real
|
|
8
|
+
package dependencies** (`@actioneer/ads-manifest`, `@actioneer/ads-lint`) so the
|
|
9
|
+
published package is **self-contained** — `npx -y @actioneer/ads-mcp` runs with no
|
|
10
|
+
repo checkout and no relative import escaping the package boundary:
|
|
11
|
+
|
|
12
|
+
- the **manifest-query core** (`@actioneer/ads-manifest`) — `(manifest, query) → results`
|
|
13
|
+
- the **lint core** — wraps `@actioneer/ads-lint` to self-check generated code
|
|
14
|
+
|
|
15
|
+
All five tools read the L1 catalog (`ads-manifest.json`).
|
|
16
|
+
|
|
17
|
+
## Tools
|
|
18
|
+
|
|
19
|
+
| Tool | Purpose |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `search_components(query, kind?, limit?)` | Enumerate what exists by name or purpose → exact export names, families, kinds, descriptions. |
|
|
22
|
+
| `get_component(name)` | Full record for one EXACT export: the exact `import { … } from "@actioneer/ads"`, props, variant/size vocabulary, showcase URL, and a copy-paste usage example. Returns an error (never invents) on an unknown name. |
|
|
23
|
+
| `get_token(role)` | The semantic token(s) for a role/utility/purpose (`brand`, `bg-danger`, `muted text`) so the agent references a token, not a literal ads-lint would reject. |
|
|
24
|
+
| `get_principles(id?, enforcedByLint?)` | The 9 ADS design principles (elevation = actionability, borderless surfaces, minimal-information-per-screen, square dots, stroke-only icons, container-fluid responsiveness, …). Each flags whether `ads-lint` auto-checks it — pair the `[lint]` ones with `lint_snippet`, self-apply the `[judgment]` ones. Teaches on-brand UX the catalog + linter can't. |
|
|
25
|
+
| `lint_snippet(code)` | Runs a snippet through `@actioneer/ads-lint` (consumer mode) and returns the verdict — deep imports, aliasing, external-UI, literals, weak motion. |
|
|
26
|
+
|
|
27
|
+
## Wiring it into Claude Code / Cursor (`.mcp.json`)
|
|
28
|
+
|
|
29
|
+
**Installed as a package** (recommended for consumer repos):
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"ads": {
|
|
35
|
+
"command": "npx",
|
|
36
|
+
"args": ["-y", "@actioneer/ads-mcp"]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`ads-mcp` resolves the catalog from the installed `@actioneer/ads/manifest.json`
|
|
43
|
+
automatically. Override with `ADS_MANIFEST`:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"mcpServers": {
|
|
48
|
+
"ads": {
|
|
49
|
+
"command": "node",
|
|
50
|
+
"args": ["tools/ads-mcp/bin/ads-mcp.mjs"],
|
|
51
|
+
"env": { "ADS_MANIFEST": "./ads-manifest.json" }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The second form is what the ADS repo itself uses for dogfooding (it points at the
|
|
58
|
+
generated repo-root `ads-manifest.json`). A ready-to-copy example lives at
|
|
59
|
+
[`mcp.example.json`](./mcp.example.json).
|
|
60
|
+
|
|
61
|
+
- **Claude Code:** drop the JSON in a `.mcp.json` at your repo root (or run
|
|
62
|
+
`claude mcp add ads -- npx -y @actioneer/ads-mcp`).
|
|
63
|
+
- **Cursor:** add the same `mcpServers` block to `.cursor/mcp.json`.
|
|
64
|
+
|
|
65
|
+
## Manifest resolution order
|
|
66
|
+
|
|
67
|
+
1. `$ADS_MANIFEST` (explicit path)
|
|
68
|
+
2. `@actioneer/ads/manifest.json` (the installed package's catalog)
|
|
69
|
+
3. `<repo>/ads-manifest.json` (local dev — generated by `npm run build:lib` / `prebuild`)
|
|
70
|
+
|
|
71
|
+
## Development
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npm run test:mcp # from the ADS repo root — tool layer, lint_snippet, stdio round-trip
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The transport is a dependency-free newline-delimited JSON-RPC 2.0 stdio loop
|
|
78
|
+
(`src/server.mjs`); all behavior lives in `src/tools.mjs` + the pure cores, which
|
|
79
|
+
are tested without ever starting the loop.
|
package/bin/ads-mcp.mjs
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ADS MCP server entry — loads the ADS manifest and starts the stdio server.
|
|
3
|
+
//
|
|
4
|
+
// Manifest resolution order (first hit wins):
|
|
5
|
+
// 1. $ADS_MANIFEST (explicit path override)
|
|
6
|
+
// 2. @actioneer/ads/manifest.json (the installed package's catalog)
|
|
7
|
+
// 3. <repo>/ads-manifest.json (local dev, generated by build)
|
|
8
|
+
//
|
|
9
|
+
// Wire it into Claude Code / Cursor via .mcp.json (see tools/ads-mcp/README.md).
|
|
10
|
+
|
|
11
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
12
|
+
import { createRequire } from "node:module";
|
|
13
|
+
import { dirname, resolve } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { startServer } from "../src/server.mjs";
|
|
16
|
+
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
|
|
20
|
+
function resolveManifestPath() {
|
|
21
|
+
if (process.env.ADS_MANIFEST && existsSync(process.env.ADS_MANIFEST)) return process.env.ADS_MANIFEST;
|
|
22
|
+
try {
|
|
23
|
+
return require.resolve("@actioneer/ads/manifest.json");
|
|
24
|
+
} catch {
|
|
25
|
+
/* not installed as a package — fall through to repo dev copy */
|
|
26
|
+
}
|
|
27
|
+
const repoCopy = resolve(__dirname, "../../../ads-manifest.json");
|
|
28
|
+
if (existsSync(repoCopy)) return repoCopy;
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const manifestPath = resolveManifestPath();
|
|
33
|
+
if (!manifestPath) {
|
|
34
|
+
console.error(
|
|
35
|
+
"ads-mcp: could not find ads-manifest.json. Set $ADS_MANIFEST, install @actioneer/ads, " +
|
|
36
|
+
"or run the build so ads-manifest.json exists at the repo root."
|
|
37
|
+
);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
42
|
+
const pkg = JSON.parse(readFileSync(resolve(__dirname, "../package.json"), "utf8"));
|
|
43
|
+
|
|
44
|
+
startServer({ manifest, name: "ads-mcp", version: pkg.version });
|
|
45
|
+
// stdio server runs until the client closes the pipe; keep the process alive.
|
|
46
|
+
process.stdin.resume();
|
package/mcp.example.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@actioneer/ads-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ADS MCP server — lets an agent in Claude Code / Cursor discover and import the Actioneer Design System through the same flow it uses for shadcn, always pointing at the canonical @actioneer/ads package (never a fork or deep import).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ads-mcp": "bin/ads-mcp.mjs"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/server.mjs",
|
|
11
|
+
"./tools": "./src/tools.mjs",
|
|
12
|
+
"./lint": "./src/lint.mjs"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"bin",
|
|
16
|
+
"src",
|
|
17
|
+
"mcp.example.json",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "node --test"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@actioneer/ads-lint": "^0.1.0",
|
|
25
|
+
"@actioneer/ads-manifest": "^0.1.0"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@actioneer/ads": "*",
|
|
29
|
+
"typescript": ">=5"
|
|
30
|
+
},
|
|
31
|
+
"peerDependenciesMeta": {
|
|
32
|
+
"@actioneer/ads": { "optional": true },
|
|
33
|
+
"typescript": { "optional": true }
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/src/lint.mjs
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// lint_snippet core — run a code string through @actioneer/ads-lint's rules in
|
|
2
|
+
// CONSUMER mode and return a structured verdict. No file IO: the ads-lint rule
|
|
3
|
+
// functions are (relPath, source, cfg) → findings, so a snippet lints directly.
|
|
4
|
+
|
|
5
|
+
// `@actioneer/ads-lint` is a real dependency (declared in package.json), so it
|
|
6
|
+
// resolves identically whether this package is installed standalone via npx or
|
|
7
|
+
// run from the monorepo (where it is linked into node_modules as a `file:` dep).
|
|
8
|
+
// No relative import escapes the package boundary.
|
|
9
|
+
import { DEFAULT_CONFIG, lintImports, lintMotion, lintLiterals } from "@actioneer/ads-lint";
|
|
10
|
+
|
|
11
|
+
const RULES = [lintImports, lintMotion, lintLiterals];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Lint a snippet as if it were a consumer file.
|
|
15
|
+
* @param {string} code
|
|
16
|
+
* @param {{filename?:string, config?:object}} [opts]
|
|
17
|
+
* @returns {{ok:boolean, errorCount:number, warningCount:number, findings:Array}}
|
|
18
|
+
*/
|
|
19
|
+
export function lintSnippet(code, opts = {}) {
|
|
20
|
+
const filename = opts.filename ?? "snippet.tsx";
|
|
21
|
+
// Consumer mode by default: every file is a consumer, all import rules apply.
|
|
22
|
+
const cfg = opts.config ?? DEFAULT_CONFIG;
|
|
23
|
+
|
|
24
|
+
const findings = [];
|
|
25
|
+
for (const rule of RULES) {
|
|
26
|
+
for (const f of rule(filename, code, cfg)) findings.push({ file: filename, ...f });
|
|
27
|
+
}
|
|
28
|
+
findings.sort((a, b) => a.line - b.line || String(a.rule).localeCompare(String(b.rule)));
|
|
29
|
+
|
|
30
|
+
const errorCount = findings.filter((f) => f.severity === "error").length;
|
|
31
|
+
const warningCount = findings.filter((f) => f.severity === "warn").length;
|
|
32
|
+
return { ok: errorCount === 0, errorCount, warningCount, findings };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Human-readable one-liner per finding (mirrors the ads-lint CLI format). */
|
|
36
|
+
export function formatVerdict(result) {
|
|
37
|
+
if (result.findings.length === 0) return "ads-lint: clean — 0 errors, 0 warnings.";
|
|
38
|
+
const lines = result.findings.map(
|
|
39
|
+
(f) => ` ${f.file}:${f.line} [${f.severity}] ${f.rule}: ${f.match} — ${f.guidance}`
|
|
40
|
+
);
|
|
41
|
+
return `ads-lint: ${result.errorCount} error(s) · ${result.warningCount} warning(s)\n${lines.join("\n")}`;
|
|
42
|
+
}
|
package/src/server.mjs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Minimal MCP stdio server — newline-delimited JSON-RPC 2.0, no SDK dependency.
|
|
2
|
+
// Wires the transport-independent tool layer (tools.mjs) to stdin/stdout. Kept
|
|
3
|
+
// deliberately thin: all behavior lives in the pure cores, which are unit-tested
|
|
4
|
+
// without ever starting this loop.
|
|
5
|
+
|
|
6
|
+
import { createTools } from "./tools.mjs";
|
|
7
|
+
|
|
8
|
+
const PROTOCOL_VERSION = "2024-11-05";
|
|
9
|
+
|
|
10
|
+
export function startServer({ manifest, name = "ads-mcp", version = "0.1.0", input = process.stdin, output = process.stdout }) {
|
|
11
|
+
const tools = createTools({ manifest });
|
|
12
|
+
|
|
13
|
+
const send = (msg) => output.write(JSON.stringify(msg) + "\n");
|
|
14
|
+
const reply = (id, result) => send({ jsonrpc: "2.0", id, result });
|
|
15
|
+
const fail = (id, code, message) => send({ jsonrpc: "2.0", id, error: { code, message } });
|
|
16
|
+
|
|
17
|
+
function handle(msg) {
|
|
18
|
+
const { id, method, params } = msg;
|
|
19
|
+
// Notifications (no id) never get a response.
|
|
20
|
+
if (id === undefined || id === null) return;
|
|
21
|
+
|
|
22
|
+
switch (method) {
|
|
23
|
+
case "initialize":
|
|
24
|
+
return reply(id, {
|
|
25
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
26
|
+
capabilities: { tools: {} },
|
|
27
|
+
serverInfo: { name, version },
|
|
28
|
+
});
|
|
29
|
+
case "tools/list":
|
|
30
|
+
return reply(id, { tools: tools.definitions });
|
|
31
|
+
case "tools/call": {
|
|
32
|
+
const result = tools.call(params?.name, params?.arguments ?? {});
|
|
33
|
+
return reply(id, result);
|
|
34
|
+
}
|
|
35
|
+
case "ping":
|
|
36
|
+
return reply(id, {});
|
|
37
|
+
default:
|
|
38
|
+
return fail(id, -32601, `Method not found: ${method}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let buffer = "";
|
|
43
|
+
input.setEncoding?.("utf8");
|
|
44
|
+
input.on("data", (chunk) => {
|
|
45
|
+
buffer += chunk;
|
|
46
|
+
let nl;
|
|
47
|
+
while ((nl = buffer.indexOf("\n")) !== -1) {
|
|
48
|
+
const line = buffer.slice(0, nl).trim();
|
|
49
|
+
buffer = buffer.slice(nl + 1);
|
|
50
|
+
if (!line) continue;
|
|
51
|
+
let msg;
|
|
52
|
+
try {
|
|
53
|
+
msg = JSON.parse(line);
|
|
54
|
+
} catch {
|
|
55
|
+
continue; // ignore malformed lines
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
handle(msg);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if (msg?.id != null) fail(msg.id, -32603, `Internal error: ${err?.message ?? err}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return { tools };
|
|
66
|
+
}
|
package/src/tools.mjs
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// The ADS MCP tool layer — transport-independent. Each tool is a thin adapter
|
|
2
|
+
// over the pure manifest-query core (tools/ads-manifest) + the lint_snippet
|
|
3
|
+
// core. `createTools({ manifest })` returns the tool definitions and a
|
|
4
|
+
// `call(name, args)` dispatcher, so the stdio server and node:test share one
|
|
5
|
+
// code path (no MCP dependency needed to test the tools).
|
|
6
|
+
|
|
7
|
+
import { searchComponents, getComponent, getToken, getPrinciples } from "@actioneer/ads-manifest/query";
|
|
8
|
+
import { lintSnippet, formatVerdict } from "./lint.mjs";
|
|
9
|
+
|
|
10
|
+
export function createTools({ manifest }) {
|
|
11
|
+
const definitions = [
|
|
12
|
+
{
|
|
13
|
+
name: "search_components",
|
|
14
|
+
description:
|
|
15
|
+
"Search the ADS catalog for components/utilities/hooks by name or purpose. " +
|
|
16
|
+
"Returns exact export names (import from '@actioneer/ads'), families, kinds, and descriptions. " +
|
|
17
|
+
"Use this first to discover what exists before importing.",
|
|
18
|
+
inputSchema: {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
query: { type: "string", description: "Name or purpose, e.g. 'button', 'date picker', 'status'." },
|
|
22
|
+
kind: { type: "string", enum: ["component", "type", "hook", "util"], description: "Optional filter." },
|
|
23
|
+
limit: { type: "number", description: "Max results (default 25)." },
|
|
24
|
+
},
|
|
25
|
+
required: ["query"],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "get_component",
|
|
30
|
+
description:
|
|
31
|
+
"Get the full record for one ADS export by its EXACT name: the exact import statement, " +
|
|
32
|
+
"props, variant/size vocabulary, showcase URL, and a copy-paste usage example. " +
|
|
33
|
+
"Returns an error if the name is not an exact export (never invents one).",
|
|
34
|
+
inputSchema: {
|
|
35
|
+
type: "object",
|
|
36
|
+
properties: { name: { type: "string", description: "Exact export name, e.g. 'Button'." } },
|
|
37
|
+
required: ["name"],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "get_token",
|
|
42
|
+
description:
|
|
43
|
+
"Get the ADS semantic token(s) for a role or purpose (e.g. 'brand', 'muted-foreground', " +
|
|
44
|
+
"'bg-danger', 'primary text'). Reference the returned token/utilities instead of a raw " +
|
|
45
|
+
"color literal — ads-lint rejects literals.",
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: "object",
|
|
48
|
+
properties: { role: { type: "string", description: "A role, utility, or purpose." } },
|
|
49
|
+
required: ["role"],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "get_principles",
|
|
54
|
+
description:
|
|
55
|
+
"Get the ADS design principles — the house rules for every ADS UI (elevation = actionability, " +
|
|
56
|
+
"borderless surfaces, minimal-information-per-screen, square status dots, stroke-only icons, " +
|
|
57
|
+
"container-fluid responsiveness, …). Each flags whether @actioneer/ads-lint auto-checks it " +
|
|
58
|
+
"(pair those with lint_snippet) or whether it is a judgment call to self-apply. Pull these to " +
|
|
59
|
+
"produce on-brand UI, not just lint-clean UI.",
|
|
60
|
+
inputSchema: {
|
|
61
|
+
type: "object",
|
|
62
|
+
properties: {
|
|
63
|
+
id: { type: "string", description: "Optional: one principle by its id or number (e.g. 'ads-first' or '7')." },
|
|
64
|
+
enforcedByLint: {
|
|
65
|
+
type: "boolean",
|
|
66
|
+
description: "Optional: only lint-enforced (true) or only judgment-call (false) principles.",
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: "lint_snippet",
|
|
73
|
+
description:
|
|
74
|
+
"Run a code snippet through @actioneer/ads-lint (consumer mode) to self-check generated ADS " +
|
|
75
|
+
"code. Flags deep imports, aliased/default/namespace barrel imports, external-UI imports, " +
|
|
76
|
+
"hardcoded color/dimension literals, and banned/weak motion. Returns the verdict.",
|
|
77
|
+
inputSchema: {
|
|
78
|
+
type: "object",
|
|
79
|
+
properties: {
|
|
80
|
+
code: { type: "string", description: "The TSX/TS code to lint." },
|
|
81
|
+
filename: { type: "string", description: "Optional filename for context (default snippet.tsx)." },
|
|
82
|
+
},
|
|
83
|
+
required: ["code"],
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
const handlers = {
|
|
89
|
+
search_components: (args) => {
|
|
90
|
+
const results = searchComponents(manifest, args.query ?? "", {
|
|
91
|
+
kind: args.kind,
|
|
92
|
+
limit: args.limit ?? 25,
|
|
93
|
+
});
|
|
94
|
+
if (results.length === 0) {
|
|
95
|
+
return text(`No ADS export matches "${args.query}". Try a broader term or a different kind.`);
|
|
96
|
+
}
|
|
97
|
+
const lines = results.map(
|
|
98
|
+
(r) => `- ${r.name} (${r.kind}, ${r.family})${r.description ? ` — ${r.description}` : ""}`
|
|
99
|
+
);
|
|
100
|
+
return text(
|
|
101
|
+
`${results.length} match(es) — import from "${manifest.importPath}":\n${lines.join("\n")}`
|
|
102
|
+
);
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
get_component: (args) => {
|
|
106
|
+
const rec = getComponent(manifest, args.name);
|
|
107
|
+
if (!rec) {
|
|
108
|
+
const near = searchComponents(manifest, args.name ?? "", { limit: 5 }).map((r) => r.name);
|
|
109
|
+
return text(
|
|
110
|
+
`"${args.name}" is not an ADS export. ${near.length ? `Did you mean: ${near.join(", ")}?` : "Use search_components to discover names."}`,
|
|
111
|
+
true
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return json(rec);
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
get_token: (args) => {
|
|
118
|
+
const tokens = getToken(manifest, args.role ?? "");
|
|
119
|
+
if (tokens.length === 0) {
|
|
120
|
+
return text(`No ADS token matches "${args.role}". Use a semantic role like 'brand' or 'muted-foreground'.`, true);
|
|
121
|
+
}
|
|
122
|
+
return json(tokens);
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
get_principles: (args) => {
|
|
126
|
+
const wantLint = typeof args.enforcedByLint === "boolean" ? args.enforcedByLint : undefined;
|
|
127
|
+
const principles = getPrinciples(manifest, { id: args.id, enforcedByLint: wantLint });
|
|
128
|
+
if (principles.length === 0) {
|
|
129
|
+
const total = manifest.principles?.length ?? 0;
|
|
130
|
+
if (total === 0) return text("No ADS principles are available in this manifest.", true);
|
|
131
|
+
if (args.id) return text(`No ADS principle matches "${args.id}". Omit filters to get all ${total}.`, true);
|
|
132
|
+
// A filter (enforcedByLint) matched nothing, though principles do exist.
|
|
133
|
+
const kind = wantLint ? "lint-enforced" : "judgment-call";
|
|
134
|
+
return text(`No ${kind} principles found. Omit filters to get all ${total}.`, true);
|
|
135
|
+
}
|
|
136
|
+
return json(principles);
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
lint_snippet: (args) => {
|
|
140
|
+
if (typeof args.code !== "string" || args.code.trim() === "") {
|
|
141
|
+
return text("lint_snippet requires a non-empty `code` string.", true);
|
|
142
|
+
}
|
|
143
|
+
const result = lintSnippet(args.code, { filename: args.filename });
|
|
144
|
+
return text(formatVerdict(result));
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
definitions,
|
|
150
|
+
/** Dispatch a tool call. @returns {{content:Array, isError?:boolean}} */
|
|
151
|
+
call(name, args = {}) {
|
|
152
|
+
const handler = handlers[name];
|
|
153
|
+
if (!handler) return text(`Unknown tool "${name}".`, true);
|
|
154
|
+
try {
|
|
155
|
+
return handler(args);
|
|
156
|
+
} catch (err) {
|
|
157
|
+
return text(`Tool "${name}" failed: ${err?.message ?? err}`, true);
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function text(t, isError = false) {
|
|
164
|
+
return { content: [{ type: "text", text: t }], ...(isError ? { isError: true } : {}) };
|
|
165
|
+
}
|
|
166
|
+
function json(obj) {
|
|
167
|
+
return { content: [{ type: "text", text: JSON.stringify(obj, null, 2) }] };
|
|
168
|
+
}
|