hakira-mcp 0.1.0 → 0.1.1

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
@@ -14,7 +14,7 @@ It exposes eight tools and one resource:
14
14
  | `get_audit_findings` | free | Finding **summaries** only. Then call `get_finding` for critical/high or when explaining/fixing. |
15
15
  | `get_finding` | free | Full finding (description, evidence, recommendation). Use after `get_audit_findings`. |
16
16
  | `list_audits` | free | Recent audits/sessions across your account (`is_current` marks this local project). Optional `workspace_id` filter. |
17
- | `list_workspaces` | free | All workspaces on your account; `is_current` marks this local project. |
17
+ | `list_workspaces` | free | All workspaces on your account; `is_current` marks this local project (`current_workspace_id` is `null` until the first audit creates one). |
18
18
  | `cancel_audit` | free | Request cancellation of a running audit. |
19
19
 
20
20
  Resource `hakira://finding/<id>` returns the same full detail as `get_finding` (kept for hosts that prefer resources).
@@ -5,3 +5,18 @@ export declare function normalizeRepoKey(url: string): string;
5
5
  * 2. else a stable uuid persisted in a locally-excluded `.hakira/mcp.json`.
6
6
  */
7
7
  export declare function resolveRepoKey(projectRoot: string): string;
8
+ /**
9
+ * READ-ONLY twin of `resolveRepoKey` — returns the key this project is ALREADY
10
+ * bound by, or null. Never mints a uuid, never writes `.hakira/mcp.json`, never
11
+ * touches `info/exclude`.
12
+ *
13
+ * The free listing tools (`list_workspaces`, `list_audits`) use this. They only
14
+ * need to LABEL which workspace is the current project; calling `resolveRepoKey`
15
+ * + `bindWorkspace` made a read-only tool create a workspace server-side, and in
16
+ * a non-git directory that meant a workspace named after a fresh uuid plus a
17
+ * `.hakira/mcp.json` dropped in a folder the user never opted in from.
18
+ *
19
+ * `start_audit` deliberately keeps `resolveRepoKey` — creating the binding is
20
+ * exactly its job.
21
+ */
22
+ export declare function peekRepoKey(projectRoot: string): string | null;
@@ -40,6 +40,36 @@ export function resolveRepoKey(projectRoot) {
40
40
  writeFileSync(file, JSON.stringify({ repo_key: id }, null, 2) + '\n');
41
41
  return id;
42
42
  }
43
+ /**
44
+ * READ-ONLY twin of `resolveRepoKey` — returns the key this project is ALREADY
45
+ * bound by, or null. Never mints a uuid, never writes `.hakira/mcp.json`, never
46
+ * touches `info/exclude`.
47
+ *
48
+ * The free listing tools (`list_workspaces`, `list_audits`) use this. They only
49
+ * need to LABEL which workspace is the current project; calling `resolveRepoKey`
50
+ * + `bindWorkspace` made a read-only tool create a workspace server-side, and in
51
+ * a non-git directory that meant a workspace named after a fresh uuid plus a
52
+ * `.hakira/mcp.json` dropped in a folder the user never opted in from.
53
+ *
54
+ * `start_audit` deliberately keeps `resolveRepoKey` — creating the binding is
55
+ * exactly its job.
56
+ */
57
+ export function peekRepoKey(projectRoot) {
58
+ const origin = tryGit(projectRoot, ['remote', 'get-url', 'origin']);
59
+ if (origin && origin.length > 0)
60
+ return normalizeRepoKey(origin);
61
+ // A uuid only counts if a PREVIOUS resolveRepoKey already persisted it.
62
+ try {
63
+ const file = join(projectRoot, '.hakira', 'mcp.json');
64
+ if (!existsSync(file))
65
+ return null;
66
+ const j = JSON.parse(readFileSync(file, 'utf8'));
67
+ return typeof j.repo_key === 'string' && j.repo_key.length > 0 ? j.repo_key : null;
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ }
43
73
  const EXCLUDE_RULE = '# Hakira MCP local id\n.hakira/\n';
44
74
  /**
45
75
  * Ignore `.hakira/` via `$GIT_DIR/info/exclude` — NOT the project's `.gitignore`.
@@ -1,12 +1,13 @@
1
1
  import { z } from 'zod';
2
2
  import { wrap, textResult } from './wrap.js';
3
- import { resolveRepoKey } from '../git/repo-key.js';
3
+ import { peekRepoKey } from '../git/repo-key.js';
4
4
  export function registerListAudits(server, deps) {
5
5
  server.registerTool('list_audits', {
6
6
  title: 'List recent Hakira audits and sessions',
7
- description: 'Free. List recent audits/sessions across your Hakira account (most recent first), with status, ' +
8
- 'workspace, origin (mcp|ui|gate), findings count and cost. `current_workspace_id` / `is_current` ' +
9
- 'mark the workspace bound to this local project. Optional `workspace_id` filters to one workspace. ' +
7
+ description: 'Free and read-only. List recent audits/sessions across your Hakira account (most recent first), ' +
8
+ 'with status, workspace, origin (mcp|ui|gate), findings count and cost. `current_workspace_id` / ' +
9
+ '`is_current` mark the workspace bound to this local project, and `current_workspace_id` is null ' +
10
+ 'when this project has no workspace yet. Optional `workspace_id` filters to one workspace. ' +
10
11
  'Use get_audit_events / get_audit_status / get_audit_findings with any owned audit_id.',
11
12
  inputSchema: {
12
13
  workspace_id: z
@@ -17,9 +18,13 @@ export function registerListAudits(server, deps) {
17
18
  },
18
19
  }, async (args) => wrap(deps, async () => {
19
20
  await deps.resolveToken();
20
- const repoKey = resolveRepoKey(deps.root);
21
- const bind = await deps.cp.bindWorkspace(repoKey);
22
- const current = bind.workspace_id;
21
+ // Match, never bind (see peekRepoKey). The workspace list is the only
22
+ // place repo_key is exposed, so resolving `current` costs one extra free
23
+ // GET worth it to keep a read-only tool read-only.
24
+ const repoKey = peekRepoKey(deps.root);
25
+ const current = repoKey
26
+ ? ((await deps.cp.listWorkspaces()).workspaces.find((w) => w.repo_key === repoKey)?.workspace_id ?? null)
27
+ : null;
23
28
  const { audits } = await deps.cp.listAudits({
24
29
  ...(args.workspace_id ? { workspace_id: args.workspace_id } : {}),
25
30
  limit: args.limit,
@@ -1,18 +1,21 @@
1
1
  import { wrap, textResult } from './wrap.js';
2
- import { resolveRepoKey } from '../git/repo-key.js';
2
+ import { peekRepoKey } from '../git/repo-key.js';
3
3
  export function registerListWorkspaces(server, deps) {
4
4
  server.registerTool('list_workspaces', {
5
5
  title: 'List all Hakira workspaces',
6
- description: 'Free. List every workspace on your Hakira account. `current_workspace_id` / `is_current` mark ' +
7
- 'the workspace bound to this local project (via git remote / .hakira/mcp.json). Use list_audits ' +
8
- 'to see sessions; start_audit always runs on the current project workspace.',
6
+ description: 'Free and read-only. List every workspace on your Hakira account. `current_workspace_id` / ' +
7
+ '`is_current` mark the workspace bound to this local project (via git remote / .hakira/mcp.json); ' +
8
+ '`current_workspace_id` is null when this project has no workspace yet — the first start_audit ' +
9
+ 'creates it. Use list_audits to see sessions; start_audit always runs on the current project workspace.',
9
10
  inputSchema: {},
10
11
  }, async () => wrap(deps, async () => {
11
12
  await deps.resolveToken();
12
- const repoKey = resolveRepoKey(deps.root);
13
- const bind = await deps.cp.bindWorkspace(repoKey);
14
- const current = bind.workspace_id;
15
13
  const { workspaces } = await deps.cp.listWorkspaces();
14
+ // Match, never bind: listing must not create a workspace as a side effect.
15
+ const repoKey = peekRepoKey(deps.root);
16
+ const current = repoKey
17
+ ? (workspaces.find((w) => w.repo_key === repoKey)?.workspace_id ?? null)
18
+ : null;
16
19
  return textResult({
17
20
  current_workspace_id: current,
18
21
  workspaces: workspaces.map((w) => ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hakira-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Hakira MCP server — trigger Hakira cloud security audits from a coding agent",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -17,10 +17,20 @@
17
17
  "ai",
18
18
  "hakira"
19
19
  ],
20
- "bin": { "hakira-mcp": "dist/index.js" },
21
- "files": ["dist", "README.md", "LICENSE"],
22
- "engines": { "node": ">=18" },
23
- "publishConfig": { "access": "public" },
20
+ "bin": {
21
+ "hakira-mcp": "dist/index.js"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "engines": {
29
+ "node": ">=18"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
24
34
  "scripts": {
25
35
  "build": "rm -rf dist && tsc && chmod +x dist/index.js",
26
36
  "dev": "tsx src/index.ts",