@ahood/cli 0.5.2 → 0.6.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 CHANGED
@@ -201,6 +201,7 @@ Sharing is additive -- it doesn't change `alice/pdf-tools`'s own public/private
201
201
  | `ahood whoami [--json]` | Reports whether your stored token still authenticates. |
202
202
  | `ahood token create <name>\|list [--json]\|revoke <id> [--yes]` | Manage personal API tokens. |
203
203
  | `ahood completion <bash\|zsh\|fish>` | Print a shell completion script for the command names. |
204
+ | `ahood mcp` | Start an MCP server exposing read-only skill commands as tools over stdio. |
204
205
 
205
206
  ### Skill
206
207
 
@@ -273,6 +274,7 @@ ahood completion fish > ~/.config/fish/completions/ahood.fish
273
274
  - **Structured output everywhere.** `--json` is available on every read command and on `publish`/`update`, so an agent never has to scrape human-formatted text.
274
275
  - **Predictable failure.** A stable, documented [exit code](#exit-codes) per failure class, and error messages that never leak raw upstream infrastructure details — safe to surface directly to an agent's reasoning loop.
275
276
  - **A real "start here."** `ahood skill init <name>` scaffolds a valid `SKILL.md` an agent can extend, rather than requiring it to know the frontmatter format up front.
277
+ - **A native MCP server, for agents that prefer tool calls to subprocess parsing.** `ahood mcp` starts a Model Context Protocol server over stdio, exposing `skill_search`, `skill_view`, `skill_read`, `skill_versions`, `skill_list`, and `whoami` as typed tools -- the same data the `--json` flags above already return, reachable as structured tool calls instead. Configure your MCP-aware agent host to run `ahood mcp` (or `npx @ahood/cli@latest mcp`) as a stdio server.
276
278
 
277
279
  ## Development
278
280
 
@@ -1,7 +1,11 @@
1
1
  import { apiJson } from "../http.js";
2
+ export async function listOwnSkills() {
3
+ const { skills } = await apiJson("/api/v1/skills?mine=true");
4
+ return skills ?? [];
5
+ }
2
6
  export async function listSkills(args = []) {
3
7
  const jsonOutput = args.includes("--json");
4
- const { skills } = await apiJson("/api/v1/skills?mine=true");
8
+ const skills = await listOwnSkills();
5
9
  if (jsonOutput) {
6
10
  console.log(JSON.stringify(skills));
7
11
  return;
@@ -0,0 +1,7 @@
1
+ import { startMcpServer } from "../mcp/server.js";
2
+ // `ahood mcp` takes no subcommands/flags in v1. Unlike every other
3
+ // account-scoped command it never returns until the client disconnects --
4
+ // see startMcpServer's comment.
5
+ export async function mcp(_args) {
6
+ await startMcpServer();
7
+ }
@@ -2,19 +2,16 @@ import { apiJson } from "../http.js";
2
2
  import { parseOwnerSkill } from "../spec.js";
3
3
  import { UsageError } from "../usage-error.js";
4
4
  const USAGE = "Usage: ahood skill read <owner>/<skill> [--json]";
5
- export async function read(args) {
6
- const jsonOutput = args.includes("--json");
7
- const spec = args.find((a) => !a.startsWith("--"));
8
- if (!spec)
9
- throw new UsageError(USAGE);
10
- const { owner, skill } = parseOwnerSkill(spec, USAGE);
5
+ export async function readSkillMd(owner, skill) {
11
6
  const detail = await apiJson(`/api/v1/skills/${encodeURIComponent(owner)}/${encodeURIComponent(skill)}`);
12
7
  if (!detail.skill_versions) {
13
8
  throw new Error(`${owner}/${skill} has no published version.`);
14
9
  }
15
10
  // Mirrors view.ts's exact wording/tone for the same warning -- this
16
11
  // command reads straight from the same endpoint/version, so it must not
17
- // be silent about handing back a yanked version's content.
12
+ // be silent about handing back a yanked version's content. Written to
13
+ // stderr, not stdout -- safe for MCP callers too (only stdout is reserved
14
+ // for the JSON-RPC stream).
18
15
  if (detail.skill_versions.yanked_at) {
19
16
  console.warn(`WARNING: ${detail.owner}/${detail.slug}@${detail.skill_versions.version} has been yanked${detail.skill_versions.yanked_reason ? `: ${detail.skill_versions.yanked_reason}` : "."}`);
20
17
  }
@@ -22,8 +19,28 @@ export async function read(args) {
22
19
  if (!content) {
23
20
  throw new Error(`${owner}/${skill}@${detail.skill_versions.version} has no SKILL.md content available.`);
24
21
  }
22
+ return {
23
+ version: detail.skill_versions.version,
24
+ content,
25
+ // The CLI already warns about a yanked version on stderr above, but an
26
+ // MCP caller has no visibility into stderr -- unlike skill_view, whose
27
+ // returned object already surfaces yanked_at/yanked_reason, skill_read
28
+ // was silently dropping this signal for MCP callers entirely. read()
29
+ // below still destructures only {version, content} for the CLI's
30
+ // --json output, so this addition doesn't change that shape.
31
+ yanked_at: detail.skill_versions.yanked_at ?? null,
32
+ yanked_reason: detail.skill_versions.yanked_reason ?? null,
33
+ };
34
+ }
35
+ export async function read(args) {
36
+ const jsonOutput = args.includes("--json");
37
+ const spec = args.find((a) => !a.startsWith("--"));
38
+ if (!spec)
39
+ throw new UsageError(USAGE);
40
+ const { owner, skill } = parseOwnerSkill(spec, USAGE);
41
+ const { version, content } = await readSkillMd(owner, skill);
25
42
  if (jsonOutput) {
26
- console.log(JSON.stringify({ version: detail.skill_versions.version, content }));
43
+ console.log(JSON.stringify({ version, content }));
27
44
  return;
28
45
  }
29
46
  // Plain mode prints the raw content verbatim -- no formatting/labels/
@@ -2,9 +2,16 @@ import { apiJson } from "../http.js";
2
2
  import { flagValue } from "../flags.js";
3
3
  import { UsageError } from "../usage-error.js";
4
4
  const USAGE = "Usage: ahood skill search <query> [--json] [--limit <n>]";
5
+ export async function searchSkills(query, limit) {
6
+ const qs = new URLSearchParams({ q: query });
7
+ if (limit !== undefined)
8
+ qs.set("per_page", String(limit));
9
+ const { skills } = await apiJson(`/api/v1/skills?${qs}`);
10
+ return skills ?? [];
11
+ }
5
12
  export async function search(args) {
6
13
  const jsonOutput = args.includes("--json");
7
- const limit = flagValue(args, "--limit");
14
+ const limitStr = flagValue(args, "--limit");
8
15
  const queryParts = args.filter((a, i) => a !== "--json" && a !== "--limit" && args[i - 1] !== "--limit");
9
16
  const unknownFlag = queryParts.find((a) => a.startsWith("--"));
10
17
  if (unknownFlag)
@@ -12,13 +19,11 @@ export async function search(args) {
12
19
  const query = queryParts.join(" ");
13
20
  if (!query)
14
21
  throw new UsageError(USAGE);
15
- if (limit !== undefined && (!/^\d+$/.test(limit) || Number(limit) < 1)) {
16
- throw new UsageError(`--limit must be a positive integer (got "${limit}").\n${USAGE}`);
22
+ if (limitStr !== undefined && (!/^\d+$/.test(limitStr) || Number(limitStr) < 1)) {
23
+ throw new UsageError(`--limit must be a positive integer (got "${limitStr}").\n${USAGE}`);
17
24
  }
18
- const qs = new URLSearchParams({ q: query });
19
- if (limit !== undefined)
20
- qs.set("per_page", limit);
21
- const { skills } = await apiJson(`/api/v1/skills?${qs}`);
25
+ const limit = limitStr !== undefined ? Number(limitStr) : undefined;
26
+ const skills = await searchSkills(query, limit);
22
27
  if (jsonOutput) {
23
28
  console.log(JSON.stringify(skills));
24
29
  return;
@@ -30,7 +35,7 @@ export async function search(args) {
30
35
  for (const skill of skills) {
31
36
  console.log(`${skill.profiles.username}/${skill.slug} - ${skill.name}${skill.tagline ? `: ${skill.tagline}` : ""} (${skill.downloads_count} downloads)`);
32
37
  }
33
- if (limit !== undefined && skills.length >= Number(limit)) {
38
+ if (limit !== undefined && skills.length >= limit) {
34
39
  console.log(`(showing up to ${limit} results -- pass a higher --limit for more)`);
35
40
  }
36
41
  }
@@ -14,13 +14,17 @@ function formatSize(bytes) {
14
14
  }
15
15
  return `${value.toFixed(1)} ${units[unitIndex]}`;
16
16
  }
17
+ export async function listSkillVersions(owner, skill) {
18
+ const { versions } = await apiJson(`/api/v1/skills/${encodeURIComponent(owner)}/${encodeURIComponent(skill)}/versions`);
19
+ return versions ?? [];
20
+ }
17
21
  export async function versions(args) {
18
22
  const jsonOutput = args.includes("--json");
19
23
  const spec = args.find((a) => !a.startsWith("--"));
20
24
  if (!spec)
21
25
  throw new UsageError(USAGE);
22
26
  const { owner, skill } = parseOwnerSkill(spec, USAGE);
23
- const { versions: list } = await apiJson(`/api/v1/skills/${encodeURIComponent(owner)}/${encodeURIComponent(skill)}/versions`);
27
+ const list = await listSkillVersions(owner, skill);
24
28
  if (jsonOutput) {
25
29
  console.log(JSON.stringify(list));
26
30
  return;
@@ -23,6 +23,9 @@ function openBrowser(url) {
23
23
  console.log(`Open this URL in your browser: ${url}`);
24
24
  }
25
25
  }
26
+ export async function viewSkill(owner, skill) {
27
+ return apiJson(`/api/v1/skills/${encodeURIComponent(owner)}/${encodeURIComponent(skill)}`);
28
+ }
26
29
  export async function view(args) {
27
30
  const jsonOutput = args.includes("--json");
28
31
  const web = args.includes("--web");
@@ -35,7 +38,7 @@ export async function view(args) {
35
38
  openBrowser(url);
36
39
  return;
37
40
  }
38
- const detail = await apiJson(`/api/v1/skills/${encodeURIComponent(owner)}/${encodeURIComponent(skill)}`);
41
+ const detail = await viewSkill(owner, skill);
39
42
  if (jsonOutput) {
40
43
  console.log(JSON.stringify(detail));
41
44
  return;
@@ -14,29 +14,20 @@ async function fetchProfile() {
14
14
  return undefined;
15
15
  }
16
16
  }
17
- export async function whoami(args = []) {
18
- const wantsJson = args.includes("--json");
17
+ // Pure auth-status check: no console output, no process.exitCode -- callers
18
+ // (the CLI's whoami() below, and the MCP whoami tool) decide how to present
19
+ // each outcome. Never throws; every failure mode is a normal return value.
20
+ export async function checkAuth() {
19
21
  const token = resolveToken();
20
22
  if (!token) {
21
- // Previously exited 0 here, defeating whoami's purpose as a scriptable
22
- // auth check -- "no token configured at all" must fail just like "token
23
- // rejected by the server" does below. Exit 4 ("authentication required
24
- // or rejected"), not the generic 1 -- this is exactly the case the
25
- // README's own exit-code table names as the canonical example of 4
26
- // (ahood-cli#80).
27
- if (wantsJson)
28
- console.log(JSON.stringify({ authenticated: false }));
29
- else
30
- console.error("Not logged in. Run `ahood login` (or set AHOOD_TOKEN).");
31
- process.exitCode = 4;
32
- return;
23
+ return { authenticated: false, reason: "not_logged_in" };
33
24
  }
34
25
  // There is no endpoint that returns an identity for a bearer caller yet, so
35
- // whoami can only answer "does this token still authenticate?". It probes
26
+ // this can only answer "does this token still authenticate?". It probes
36
27
  // /api/v1/auth/tokens, which is deliberately session-only (Task 3), and
37
28
  // reads the STATUS to tell the two failure modes apart -- swallowing every
38
- // error and printing "Authenticated." unconditionally (as this did) meant a
39
- // revoked token and a garbage token both reported success with exit code 0.
29
+ // error and reporting success unconditionally (as this used to) meant a
30
+ // revoked token and a garbage token both reported success.
40
31
  // 403 -> resolveCaller accepted the token, the route then rejected it for
41
32
  // being a token rather than a session. The token is valid.
42
33
  // 401 -> resolveCaller could not resolve the token at all: unknown,
@@ -48,38 +39,66 @@ export async function whoami(args = []) {
48
39
  // A session-backed caller (only reachable if this ever runs against a
49
40
  // cookie-bearing client) -- the token list came back.
50
41
  const profile = await fetchProfile();
51
- if (wantsJson)
52
- console.log(JSON.stringify({ authenticated: true, mode: "session", ...profile }));
53
- else
54
- console.log(profile ? `Authenticated as ${profile.username}.` : "Authenticated.");
42
+ return { authenticated: true, mode: "session", profile };
55
43
  }
56
44
  catch (error) {
57
45
  if (error instanceof ApiError && error.status === 403) {
58
46
  const profile = await fetchProfile();
59
- if (wantsJson)
60
- console.log(JSON.stringify({ authenticated: true, mode: "token", ...profile }));
61
- else
62
- console.log(profile
63
- ? `Authenticated as ${profile.username} (personal API token).`
64
- : "Authenticated with a personal API token.");
65
- return;
47
+ return { authenticated: true, mode: "token", profile };
66
48
  }
67
49
  if (error instanceof ApiError && error.status === 401) {
68
- // Same exit-4 reasoning as the "no token at all" branch above: the
69
- // token was rejected by the server, which is the other half of the
70
- // README's exit-code-4 definition ("or the token was refused").
71
- if (wantsJson)
72
- console.log(JSON.stringify({ authenticated: false, reason: "invalid_token" }));
73
- else
74
- console.error("Not authenticated -- your token is invalid or has been revoked.");
75
- process.exitCode = 4;
76
- return;
50
+ return { authenticated: false, reason: "invalid_token" };
77
51
  }
78
52
  const message = error instanceof Error ? error.message : String(error);
53
+ return { authenticated: null, error: message };
54
+ }
55
+ }
56
+ export async function whoami(args = []) {
57
+ const wantsJson = args.includes("--json");
58
+ const result = await checkAuth();
59
+ if (result.authenticated === false && result.reason === "not_logged_in") {
60
+ // Previously exited 0 here, defeating whoami's purpose as a scriptable
61
+ // auth check -- "no token configured at all" must fail just like "token
62
+ // rejected by the server" does below. Exit 4 ("authentication required
63
+ // or rejected"), not the generic 1 -- this is exactly the case the
64
+ // README's own exit-code table names as the canonical example of 4
65
+ // (ahood-cli#80).
66
+ if (wantsJson)
67
+ console.log(JSON.stringify({ authenticated: false }));
68
+ else
69
+ console.error("Not logged in. Run `ahood login` (or set AHOOD_TOKEN).");
70
+ process.exitCode = 4;
71
+ return;
72
+ }
73
+ if (result.authenticated === false && result.reason === "invalid_token") {
74
+ // Same exit-4 reasoning as the "no token at all" branch above: the
75
+ // token was rejected by the server, which is the other half of the
76
+ // README's exit-code-4 definition ("or the token was refused").
79
77
  if (wantsJson)
80
- console.log(JSON.stringify({ authenticated: null, error: message }));
78
+ console.log(JSON.stringify({ authenticated: false, reason: "invalid_token" }));
81
79
  else
82
- console.error(`Could not verify your token: ${message}`);
80
+ console.error("Not authenticated -- your token is invalid or has been revoked.");
81
+ process.exitCode = 4;
82
+ return;
83
+ }
84
+ if (result.authenticated === null) {
85
+ if (wantsJson)
86
+ console.log(JSON.stringify({ authenticated: null, error: result.error }));
87
+ else
88
+ console.error(`Could not verify your token: ${result.error}`);
83
89
  process.exitCode = 1;
90
+ return;
91
+ }
92
+ if (wantsJson) {
93
+ console.log(JSON.stringify({ authenticated: true, mode: result.mode, ...result.profile }));
94
+ return;
95
+ }
96
+ if (result.mode === "session") {
97
+ console.log(result.profile ? `Authenticated as ${result.profile.username}.` : "Authenticated.");
98
+ }
99
+ else {
100
+ console.log(result.profile
101
+ ? `Authenticated as ${result.profile.username} (personal API token).`
102
+ : "Authenticated with a personal API token.");
84
103
  }
85
104
  }
package/dist/help.js CHANGED
@@ -26,6 +26,14 @@ export const TOP_LEVEL_COMMANDS_HELP = [
26
26
  desc: "Print a shell completion script for the command names.",
27
27
  examples: ["ahood completion bash >> ~/.bashrc"],
28
28
  },
29
+ {
30
+ usage: "ahood mcp",
31
+ summary: "Start an MCP server exposing read-only skill commands as tools over stdio.",
32
+ desc: "Start a Model Context Protocol server over stdio, exposing skill_search, skill_view, skill_read, " +
33
+ "skill_versions, skill_list, and whoami as MCP tools -- the same data ahood's --json commands already " +
34
+ "return, reachable as typed tool calls instead of parsed CLI output. Meant to be launched by an " +
35
+ "MCP-aware agent host (e.g. Claude Code's MCP server configuration), not run interactively.",
36
+ },
29
37
  ];
30
38
  export const SKILL_COMMANDS_HELP = [
31
39
  {
package/dist/index.js CHANGED
@@ -18,6 +18,7 @@ import { view } from "./commands/view.js";
18
18
  import { read } from "./commands/read.js";
19
19
  import { versions } from "./commands/versions.js";
20
20
  import { completion } from "./commands/completion.js";
21
+ import { mcp } from "./commands/mcp.js";
21
22
  import { init } from "./commands/init.js";
22
23
  import { createGroup, listGroups, groupMembers, inviteLink, joinGroup, removeMember, leaveGroup, deleteGroup, } from "./commands/group.js";
23
24
  import { formatHelp, formatSkillHelp, formatGroupHelp, formatCommandHelp, findCommandHelp } from "./help.js";
@@ -69,6 +70,7 @@ const COMMANDS = {
69
70
  whoami,
70
71
  token,
71
72
  completion,
73
+ mcp,
72
74
  skill: dispatchSkill,
73
75
  group: dispatchGroup,
74
76
  };
@@ -0,0 +1,46 @@
1
+ import { ApiError, NetworkError } from "../http.js";
2
+ import { UsageError } from "../usage-error.js";
3
+ // Mirrors exit-code.ts's exitCodeFor categorization (ahood-cli#83) -- one
4
+ // error taxonomy for both the CLI's exit codes and MCP's error_code field,
5
+ // rather than maintaining two.
6
+ export function errorCodeFor(error) {
7
+ if (error instanceof UsageError)
8
+ return "usage_error";
9
+ if (error instanceof ApiError) {
10
+ if (error.status === 401 || error.status === 403)
11
+ return "auth_error";
12
+ if (error.status === 404)
13
+ return "not_found";
14
+ if (error.status >= 500)
15
+ return "network_error";
16
+ return "usage_error";
17
+ }
18
+ if (error instanceof NetworkError)
19
+ return "network_error";
20
+ return "general_error";
21
+ }
22
+ // Wraps an MCP tool's data-fetching logic so a thrown error never crosses
23
+ // the stdio boundary as a raw exception -- caught here and turned into a
24
+ // structured isError result instead. `fn` is the tool's own core function
25
+ // (e.g. searchSkills), already adapted to take the single zod-validated
26
+ // input object each MCP tool callback receives.
27
+ export function safeTool(fn) {
28
+ return async (input) => {
29
+ try {
30
+ const result = await fn(input);
31
+ // Defensive backstop: JSON.stringify(undefined) evaluates to the JS
32
+ // value `undefined`, not a string -- if any tool's core function ever
33
+ // returns undefined (e.g. a malformed API response missing an
34
+ // expected key), the MCP SDK rejects that as an invalid tool result
35
+ // and throws a raw protocol-level error instead of the structured
36
+ // isError result this wrapper exists to guarantee. Coalescing to null
37
+ // keeps `text` a string no matter what `fn` returns.
38
+ return { content: [{ type: "text", text: JSON.stringify(result ?? null) }] };
39
+ }
40
+ catch (error) {
41
+ const message = error instanceof Error ? error.message : String(error);
42
+ const body = { error: message, error_code: errorCodeFor(error) };
43
+ return { content: [{ type: "text", text: JSON.stringify(body) }], isError: true };
44
+ }
45
+ };
46
+ }
@@ -0,0 +1,20 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { registerTools } from "./tools.js";
4
+ import { CLI_VERSION } from "../version.js";
5
+ // Split from startMcpServer so tests can build a server and connect it to an
6
+ // InMemoryTransport instead of real stdio.
7
+ export function buildServer() {
8
+ const server = new McpServer({ name: "ahood", version: CLI_VERSION });
9
+ registerTools(server);
10
+ return server;
11
+ }
12
+ // Connects over stdio and blocks: StdioServerTransport reads process.stdin
13
+ // for the life of the process. This is expected -- an MCP host spawns
14
+ // `ahood mcp` as a long-lived subprocess and talks to it over stdio for as
15
+ // long as the session lasts.
16
+ export async function startMcpServer() {
17
+ const server = buildServer();
18
+ const transport = new StdioServerTransport();
19
+ await server.connect(transport);
20
+ }
@@ -0,0 +1,61 @@
1
+ import { z } from "zod";
2
+ import { safeTool } from "./safe-tool.js";
3
+ import { searchSkills } from "../commands/search.js";
4
+ import { viewSkill } from "../commands/view.js";
5
+ import { readSkillMd } from "../commands/read.js";
6
+ import { listSkillVersions } from "../commands/versions.js";
7
+ import { listOwnSkills } from "../commands/list.js";
8
+ import { checkAuth } from "../commands/whoami.js";
9
+ import { validateSegment } from "../spec.js";
10
+ // MCP tool inputs arrive as separate owner/skill zod fields (not a single
11
+ // "<owner>/<skill>" spec string like the CLI's argv), so they bypass
12
+ // parseOwnerSkill entirely and go straight to the core function -- which
13
+ // URL-encodes them and hits the API. Running the same validateSegment checks
14
+ // the CLI gets via parseOwnerSkill here closes that gap: an invalid segment
15
+ // (e.g. "..") or an absurdly long one now fails locally as a UsageError
16
+ // (safeTool maps it to error_code "usage_error") instead of reaching the
17
+ // wire with the caller's bearer token.
18
+ function validateOwnerAndSkill(owner, skill) {
19
+ const spec = `${owner}/${skill}`;
20
+ validateSegment(owner, "owner", spec);
21
+ validateSegment(skill, "skill", spec);
22
+ }
23
+ // Registers every v1 MCP tool. Read-only by design (ahood-cli#83's spec) --
24
+ // each tool wraps the same core function the equivalent `ahood skill <verb>
25
+ // --json` CLI path already calls, so there is exactly one source of truth
26
+ // for what each of these returns.
27
+ export function registerTools(server) {
28
+ server.registerTool("skill_search", {
29
+ description: "Search published skills in the ahood registry.",
30
+ inputSchema: { query: z.string().min(1), limit: z.number().int().positive().optional() },
31
+ }, safeTool(async ({ query, limit }) => searchSkills(query, limit)));
32
+ server.registerTool("skill_view", {
33
+ description: "Show a skill's details (tags, license, homepage, repository, dates, etc.) without installing it.",
34
+ inputSchema: { owner: z.string().min(1), skill: z.string().min(1) },
35
+ }, safeTool(async ({ owner, skill }) => {
36
+ validateOwnerAndSkill(owner, skill);
37
+ return viewSkill(owner, skill);
38
+ }));
39
+ server.registerTool("skill_read", {
40
+ description: "Read a skill's full SKILL.md content without installing it.",
41
+ inputSchema: { owner: z.string().min(1), skill: z.string().min(1) },
42
+ }, safeTool(async ({ owner, skill }) => {
43
+ validateOwnerAndSkill(owner, skill);
44
+ return readSkillMd(owner, skill);
45
+ }));
46
+ server.registerTool("skill_versions", {
47
+ description: "List a skill's published-version history (version, changelog, size, publish date).",
48
+ inputSchema: { owner: z.string().min(1), skill: z.string().min(1) },
49
+ }, safeTool(async ({ owner, skill }) => {
50
+ validateOwnerAndSkill(owner, skill);
51
+ return listSkillVersions(owner, skill);
52
+ }));
53
+ server.registerTool("skill_list", {
54
+ description: "List the authenticated caller's own skills, public and private.",
55
+ inputSchema: {},
56
+ }, safeTool(async () => listOwnSkills()));
57
+ server.registerTool("whoami", {
58
+ description: "Report whether the configured token (AHOOD_TOKEN or a stored login) still authenticates.",
59
+ inputSchema: {},
60
+ }, safeTool(async () => checkAuth()));
61
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ahood/cli",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "CLI for ahood, a registry for installing and publishing Claude Code skills.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/AlexKay28/ahood-cli",
@@ -32,7 +32,9 @@
32
32
  "sync-readme": "node scripts/sync-readme.mjs"
33
33
  },
34
34
  "dependencies": {
35
- "tar-stream": "^3.2.1"
35
+ "@modelcontextprotocol/sdk": "^1.30.0",
36
+ "tar-stream": "^3.2.1",
37
+ "zod": "^4.5.4"
36
38
  },
37
39
  "devDependencies": {
38
40
  "@types/node": "^20",