@okmira/mcp 0.0.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 ADDED
@@ -0,0 +1,95 @@
1
+ # @okmira/mcp
2
+
3
+ MCP server and Claude Code hooks for [Mira](https://okmira.ai) — structured stakeholder feedback for AI-assisted development.
4
+
5
+ ## What is Mira?
6
+
7
+ AI made developers 10x faster. Feedback is still 1x. Mira closes the gap.
8
+
9
+ Mira structures stakeholder feedback so AI coding assistants can act on it directly. Stakeholders review deployed builds and leave role-contextualized feedback ("What works" / "What doesn't" / "Suggested direction"). Developers fetch that feedback into Claude Code and apply it conversationally.
10
+
11
+ ## Installation
12
+
13
+ Register the MCP server with Claude Code (takes effect immediately, no restart):
14
+
15
+ ```bash
16
+ claude mcp add mira \
17
+ -e CC_FEEDBACK_API_URL=https://okmira.ai \
18
+ -e CC_FEEDBACK_API_KEY=ccf_YOUR_KEY_HERE \
19
+ -- npx @okmira/mcp
20
+ ```
21
+
22
+ Get your API key from the project dashboard at [okmira.ai](https://okmira.ai).
23
+
24
+ ## MCP Tools
25
+
26
+ | Tool | Description |
27
+ |------|-------------|
28
+ | `register_build` | Register a new build after deployment. Returns a review URL for stakeholders. |
29
+ | `fetch_feedback` | Fetch structured feedback for a build. Returns role-contextualized feedback with suggested directions. |
30
+ | `update_feedback_status` | Mark feedback as acknowledged, applied, or stale. Closes the feedback loop. |
31
+ | `check_pending_feedback` | Check for unreviewed feedback across recent builds. Acts as an inbox. |
32
+
33
+ ## Claude Code Hooks
34
+
35
+ Optional hooks for zero-friction automation. Add a `.mira.json` to your project root:
36
+
37
+ ```json
38
+ {
39
+ "projectId": "your-project-uuid"
40
+ }
41
+ ```
42
+
43
+ Then add to `.claude/settings.json`:
44
+
45
+ ```json
46
+ {
47
+ "hooks": {
48
+ "PostToolUse": [
49
+ {
50
+ "matcher": "Bash",
51
+ "hooks": [{ "type": "command", "command": "mira-auto-register" }]
52
+ }
53
+ ],
54
+ "SessionStart": [
55
+ {
56
+ "matcher": "",
57
+ "hooks": [{ "type": "command", "command": "mira-check-feedback" }]
58
+ }
59
+ ]
60
+ }
61
+ }
62
+ ```
63
+
64
+ **`mira-auto-register`** (PostToolUse) — Detects Vercel deploy URLs in Bash output and automatically registers builds with Mira. Exits immediately for non-deploy commands.
65
+
66
+ **`mira-check-feedback`** (SessionStart) — Surfaces pending feedback count at the start of each session so Claude can proactively inform you.
67
+
68
+ Hook credentials are read from the MCP server config — no duplication needed.
69
+
70
+ ## Configuration
71
+
72
+ | Variable | Description |
73
+ |----------|-------------|
74
+ | `CC_FEEDBACK_API_URL` | Base URL — `https://okmira.ai` (production) or `http://localhost:3000` (local) |
75
+ | `CC_FEEDBACK_API_KEY` | Project-scoped API key (`ccf_` prefix), generated from the dashboard |
76
+
77
+ ## How It Works
78
+
79
+ ```
80
+ Developer deploys build
81
+ → Build registered (via MCP tool or auto-register hook)
82
+ → Stakeholders notified, review build, leave structured feedback
83
+ → Developer fetches feedback into Claude Code
84
+ → Claude applies feedback conversationally
85
+ → New build deployed → cycle repeats
86
+ ```
87
+
88
+ ## Requirements
89
+
90
+ - Node.js 18+ (for native `fetch()`)
91
+ - `git` (for hooks — ref/branch detection)
92
+
93
+ ## License
94
+
95
+ MIT
@@ -0,0 +1,10 @@
1
+ export declare class ApiClient {
2
+ private baseUrl;
3
+ private apiKey;
4
+ constructor(baseUrl: string, apiKey: string);
5
+ request<T>(method: string, path: string, body?: unknown): Promise<T>;
6
+ get<T>(path: string): Promise<T>;
7
+ post<T>(path: string, body: unknown): Promise<T>;
8
+ patch<T>(path: string, body: unknown): Promise<T>;
9
+ }
10
+ //# sourceMappingURL=api-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"AAAA,qBAAa,SAAS;IACpB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,MAAM,CAAS;gBAEX,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAKrC,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IAqB1E,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC;IAIhC,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;IAIhD,KAAK,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC;CAGlD"}
@@ -0,0 +1,34 @@
1
+ export class ApiClient {
2
+ baseUrl;
3
+ apiKey;
4
+ constructor(baseUrl, apiKey) {
5
+ this.baseUrl = baseUrl.replace(/\/$/, "");
6
+ this.apiKey = apiKey;
7
+ }
8
+ async request(method, path, body) {
9
+ const url = `${this.baseUrl}${path}`;
10
+ const response = await fetch(url, {
11
+ method,
12
+ headers: {
13
+ "Content-Type": "application/json",
14
+ Authorization: `Bearer ${this.apiKey}`,
15
+ },
16
+ body: body ? JSON.stringify(body) : undefined,
17
+ });
18
+ if (!response.ok) {
19
+ const errBody = (await response.json().catch(() => ({ error: response.statusText })));
20
+ throw new Error(`API error ${response.status}: ${errBody.error || response.statusText}`);
21
+ }
22
+ return response.json();
23
+ }
24
+ get(path) {
25
+ return this.request("GET", path);
26
+ }
27
+ post(path, body) {
28
+ return this.request("POST", path, body);
29
+ }
30
+ patch(path, body) {
31
+ return this.request("PATCH", path, body);
32
+ }
33
+ }
34
+ //# sourceMappingURL=api-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-client.js","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,SAAS;IACZ,OAAO,CAAS;IAChB,MAAM,CAAS;IAEvB,YAAY,OAAe,EAAE,MAAc;QACzC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QAC1C,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;IAED,KAAK,CAAC,OAAO,CAAI,MAAc,EAAE,IAAY,EAAE,IAAc;QAC3D,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,OAAO,GAAG,IAAI,EAAE,CAAC;QACrC,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;YAChC,MAAM;YACN,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,aAAa,EAAE,UAAU,IAAI,CAAC,MAAM,EAAE;aACvC;YACD,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS;SAC9C,CAAC,CAAC;QAEH,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;YACjB,MAAM,OAAO,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC,CAEnF,CAAC;YACF,MAAM,IAAI,KAAK,CAAC,aAAa,QAAQ,CAAC,MAAM,KAAK,OAAO,CAAC,KAAK,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC,CAAC;QAC3F,CAAC;QAED,OAAO,QAAQ,CAAC,IAAI,EAAgB,CAAC;IACvC,CAAC;IAED,GAAG,CAAI,IAAY;QACjB,OAAO,IAAI,CAAC,OAAO,CAAI,KAAK,EAAE,IAAI,CAAC,CAAC;IACtC,CAAC;IAED,IAAI,CAAI,IAAY,EAAE,IAAa;QACjC,OAAO,IAAI,CAAC,OAAO,CAAI,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IAC7C,CAAC;IAED,KAAK,CAAI,IAAY,EAAE,IAAa;QAClC,OAAO,IAAI,CAAC,OAAO,CAAI,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IAC9C,CAAC;CACF"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { ApiClient } from "./api-client.js";
5
+ import { registerBuildTool } from "./tools/register-build.js";
6
+ import { fetchFeedbackTool } from "./tools/fetch-feedback.js";
7
+ import { updateFeedbackStatusTool } from "./tools/update-feedback-status.js";
8
+ import { checkPendingTool } from "./tools/check-pending.js";
9
+ const server = new McpServer({
10
+ name: "cc-feedback",
11
+ version: "0.0.1",
12
+ });
13
+ const apiUrl = process.env.CC_FEEDBACK_API_URL || "http://localhost:3000";
14
+ const apiKey = process.env.CC_FEEDBACK_API_KEY || "";
15
+ if (!apiKey) {
16
+ console.error("Warning: CC_FEEDBACK_API_KEY is not set. API calls will fail.");
17
+ }
18
+ const api = new ApiClient(apiUrl, apiKey);
19
+ // Register all tools
20
+ registerBuildTool(server, api);
21
+ fetchFeedbackTool(server, api);
22
+ updateFeedbackStatusTool(server, api);
23
+ checkPendingTool(server, api);
24
+ async function main() {
25
+ const transport = new StdioServerTransport();
26
+ await server.connect(transport);
27
+ console.error("cc-feedback MCP server running on stdio");
28
+ }
29
+ main().catch(console.error);
30
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,wBAAwB,EAAE,MAAM,mCAAmC,CAAC;AAC7E,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAE5D,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;IAC3B,IAAI,EAAE,aAAa;IACnB,OAAO,EAAE,OAAO;CACjB,CAAC,CAAC;AAEH,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,uBAAuB,CAAC;AAC1E,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,mBAAmB,IAAI,EAAE,CAAC;AAErD,IAAI,CAAC,MAAM,EAAE,CAAC;IACZ,OAAO,CAAC,KAAK,CAAC,+DAA+D,CAAC,CAAC;AACjF,CAAC;AAED,MAAM,GAAG,GAAG,IAAI,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE1C,qBAAqB;AACrB,iBAAiB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAC/B,iBAAiB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAC/B,wBAAwB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AACtC,gBAAgB,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAE9B,KAAK,UAAU,IAAI;IACjB,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,OAAO,CAAC,KAAK,CAAC,yCAAyC,CAAC,CAAC;AAC3D,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ApiClient } from "../api-client.js";
3
+ export declare function checkPendingTool(server: McpServer, api: ApiClient): void;
4
+ //# sourceMappingURL=check-pending.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check-pending.d.ts","sourceRoot":"","sources":["../../src/tools/check-pending.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGlD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,QA6BjE"}
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+ export function checkPendingTool(server, api) {
3
+ server.tool("check_pending_feedback", "Check for unreviewed feedback across recent builds. Acts as an inbox to surface feedback that needs attention.", {
4
+ project_id: z.string().uuid().describe("The project UUID to check"),
5
+ }, async ({ project_id }) => {
6
+ const response = await api.get(`/api/v1/projects/${project_id}/feedback/pending`);
7
+ let text;
8
+ if (response.pending_count === 0) {
9
+ text = `No pending feedback for ${response.project}.`;
10
+ }
11
+ else {
12
+ text = JSON.stringify(response, null, 2);
13
+ }
14
+ return {
15
+ content: [
16
+ {
17
+ type: "text",
18
+ text,
19
+ },
20
+ ],
21
+ };
22
+ });
23
+ }
24
+ //# sourceMappingURL=check-pending.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check-pending.js","sourceRoot":"","sources":["../../src/tools/check-pending.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,MAAM,UAAU,gBAAgB,CAAC,MAAiB,EAAE,GAAc;IAChE,MAAM,CAAC,IAAI,CACT,wBAAwB,EACxB,gHAAgH,EAChH;QACE,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,2BAA2B,CAAC;KACpE,EACD,KAAK,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE;QACvB,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,GAAG,CAC5B,oBAAoB,UAAU,mBAAmB,CAClD,CAAC;QAEF,IAAI,IAAY,CAAC;QACjB,IAAI,QAAQ,CAAC,aAAa,KAAK,CAAC,EAAE,CAAC;YACjC,IAAI,GAAG,2BAA2B,QAAQ,CAAC,OAAO,GAAG,CAAC;QACxD,CAAC;aAAM,CAAC;YACN,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;QAC3C,CAAC;QAED,OAAO;YACL,OAAO,EAAE;gBACP;oBACE,IAAI,EAAE,MAAe;oBACrB,IAAI;iBACL;aACF;SACF,CAAC;IACJ,CAAC,CACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ApiClient } from "../api-client.js";
3
+ export declare function fetchFeedbackTool(server: McpServer, api: ApiClient): void;
4
+ //# sourceMappingURL=fetch-feedback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-feedback.d.ts","sourceRoot":"","sources":["../../src/tools/fetch-feedback.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGlD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,QAmClE"}
@@ -0,0 +1,32 @@
1
+ import { z } from "zod";
2
+ export function fetchFeedbackTool(server, api) {
3
+ server.tool("fetch_feedback", "Fetch structured feedback for a build. Returns role-contextualized feedback with suggested directions for AI consumption.", {
4
+ build_id: z.string().uuid().describe("The build UUID to fetch feedback for"),
5
+ status: z
6
+ .enum(["pending", "acknowledged", "applied", "stale"])
7
+ .optional()
8
+ .describe("Filter by feedback status"),
9
+ type: z
10
+ .enum(["internal", "external"])
11
+ .optional()
12
+ .describe("Filter by member type (internal team vs external client)"),
13
+ }, async ({ build_id, status, type }) => {
14
+ const params = new URLSearchParams();
15
+ if (status)
16
+ params.set("status", status);
17
+ if (type)
18
+ params.set("type", type);
19
+ const qs = params.toString();
20
+ const path = `/api/v1/builds/${build_id}/feedback${qs ? `?${qs}` : ""}`;
21
+ const response = await api.get(path);
22
+ return {
23
+ content: [
24
+ {
25
+ type: "text",
26
+ text: JSON.stringify(response, null, 2),
27
+ },
28
+ ],
29
+ };
30
+ });
31
+ }
32
+ //# sourceMappingURL=fetch-feedback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fetch-feedback.js","sourceRoot":"","sources":["../../src/tools/fetch-feedback.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,MAAM,UAAU,iBAAiB,CAAC,MAAiB,EAAE,GAAc;IACjE,MAAM,CAAC,IAAI,CACT,gBAAgB,EAChB,2HAA2H,EAC3H;QACE,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,sCAAsC,CAAC;QAC5E,MAAM,EAAE,CAAC;aACN,IAAI,CAAC,CAAC,SAAS,EAAE,cAAc,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;aACrD,QAAQ,EAAE;aACV,QAAQ,CAAC,2BAA2B,CAAC;QACxC,IAAI,EAAE,CAAC;aACJ,IAAI,CAAC,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;aAC9B,QAAQ,EAAE;aACV,QAAQ,CAAC,0DAA0D,CAAC;KACxE,EACD,KAAK,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,EAAE;QACnC,MAAM,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;QACrC,IAAI,MAAM;YAAE,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QACzC,IAAI,IAAI;YAAE,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAEnC,MAAM,EAAE,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC;QAC7B,MAAM,IAAI,GAAG,kBAAkB,QAAQ,YAAY,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QAExE,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,GAAG,CAAwB,IAAI,CAAC,CAAC;QAE5D,OAAO;YACL,OAAO,EAAE;gBACP;oBACE,IAAI,EAAE,MAAe;oBACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;iBACxC;aACF;SACF,CAAC;IACJ,CAAC,CACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ApiClient } from "../api-client.js";
3
+ export declare function registerBuildTool(server: McpServer, api: ApiClient): void;
4
+ //# sourceMappingURL=register-build.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register-build.d.ts","sourceRoot":"","sources":["../../src/tools/register-build.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGlD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,QAoClE"}
@@ -0,0 +1,29 @@
1
+ import { z } from "zod";
2
+ export function registerBuildTool(server, api) {
3
+ server.tool("register_build", "Register a new build after deployment. Returns a review URL for stakeholders.", {
4
+ project_id: z.string().uuid().describe("The project UUID"),
5
+ url: z.string().url().describe("Deploy URL of the build"),
6
+ git_ref: z.string().describe("Git commit SHA or ref"),
7
+ track: z.string().describe("Branch or feature track name"),
8
+ }, async ({ project_id, url, git_ref, track }) => {
9
+ const build = await api.post(`/api/v1/projects/${project_id}/builds`, {
10
+ url,
11
+ git_ref,
12
+ track,
13
+ });
14
+ return {
15
+ content: [
16
+ {
17
+ type: "text",
18
+ text: JSON.stringify({
19
+ build_id: build.id,
20
+ url: build.url,
21
+ deployed_at: build.deployed_at,
22
+ review_url: build.review_url,
23
+ }, null, 2),
24
+ },
25
+ ],
26
+ };
27
+ });
28
+ }
29
+ //# sourceMappingURL=register-build.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register-build.js","sourceRoot":"","sources":["../../src/tools/register-build.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,MAAM,UAAU,iBAAiB,CAAC,MAAiB,EAAE,GAAc;IACjE,MAAM,CAAC,IAAI,CACT,gBAAgB,EAChB,+EAA+E,EAC/E;QACE,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,kBAAkB,CAAC;QAC1D,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,yBAAyB,CAAC;QACzD,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,uBAAuB,CAAC;QACrD,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,8BAA8B,CAAC;KAC3D,EACD,KAAK,EAAE,EAAE,UAAU,EAAE,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE;QAC5C,MAAM,KAAK,GAAG,MAAM,GAAG,CAAC,IAAI,CAAgB,oBAAoB,UAAU,SAAS,EAAE;YACnF,GAAG;YACH,OAAO;YACP,KAAK;SACN,CAAC,CAAC;QAEH,OAAO;YACL,OAAO,EAAE;gBACP;oBACE,IAAI,EAAE,MAAe;oBACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAClB;wBACE,QAAQ,EAAE,KAAK,CAAC,EAAE;wBAClB,GAAG,EAAE,KAAK,CAAC,GAAG;wBACd,WAAW,EAAE,KAAK,CAAC,WAAW;wBAC9B,UAAU,EAAE,KAAK,CAAC,UAAU;qBAC7B,EACD,IAAI,EACJ,CAAC,CACF;iBACF;aACF;SACF,CAAC;IACJ,CAAC,CACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { ApiClient } from "../api-client.js";
3
+ export declare function updateFeedbackStatusTool(server: McpServer, api: ApiClient): void;
4
+ //# sourceMappingURL=update-feedback-status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"update-feedback-status.d.ts","sourceRoot":"","sources":["../../src/tools/update-feedback-status.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACzE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGlD,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,QAwBzE"}
@@ -0,0 +1,21 @@
1
+ import { z } from "zod";
2
+ export function updateFeedbackStatusTool(server, api) {
3
+ server.tool("update_feedback_status", "Update the status of one or more feedback items. Use after reviewing or applying feedback.", {
4
+ feedback_ids: z.array(z.string().uuid()).min(1).describe("Array of feedback UUIDs to update"),
5
+ status: z.enum(["acknowledged", "applied", "stale"]).describe("New status to set"),
6
+ }, async ({ feedback_ids, status }) => {
7
+ const response = await api.patch("/api/v1/feedback", {
8
+ feedback_ids,
9
+ status,
10
+ });
11
+ return {
12
+ content: [
13
+ {
14
+ type: "text",
15
+ text: JSON.stringify(response, null, 2),
16
+ },
17
+ ],
18
+ };
19
+ });
20
+ }
21
+ //# sourceMappingURL=update-feedback-status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"update-feedback-status.js","sourceRoot":"","sources":["../../src/tools/update-feedback-status.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,MAAM,UAAU,wBAAwB,CAAC,MAAiB,EAAE,GAAc;IACxE,MAAM,CAAC,IAAI,CACT,wBAAwB,EACxB,4FAA4F,EAC5F;QACE,YAAY,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,mCAAmC,CAAC;QAC7F,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,cAAc,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,mBAAmB,CAAC;KACnF,EACD,KAAK,EAAE,EAAE,YAAY,EAAE,MAAM,EAAE,EAAE,EAAE;QACjC,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,KAAK,CAA+B,kBAAkB,EAAE;YACjF,YAAY;YACZ,MAAM;SACP,CAAC,CAAC;QAEH,OAAO;YACL,OAAO,EAAE;gBACP;oBACE,IAAI,EAAE,MAAe;oBACrB,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;iBACxC;aACF;SACF,CAAC;IACJ,CAAC,CACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env node
2
+
3
+ // PostToolUse hook: auto-register Vercel deploys with Mira.
4
+ //
5
+ // Fires after every tool use. Exits immediately for non-deploy
6
+ // commands. When a Vercel deploy URL is detected, registers the build
7
+ // and returns context so Claude can inform the developer.
8
+
9
+ import {
10
+ readStdin,
11
+ readMiraConfig,
12
+ readMcpCredentials,
13
+ miraApi,
14
+ getGitInfo,
15
+ } from "./lib.js";
16
+
17
+ try {
18
+ const input = readStdin();
19
+ if (!input) process.exit(0);
20
+
21
+ // Only process Bash tool executions
22
+ if (input.tool_name !== "Bash") process.exit(0);
23
+
24
+ // Get tool output (handles both string and object formats)
25
+ let toolOutput = "";
26
+ const out = input.tool_output;
27
+ if (typeof out === "string") {
28
+ toolOutput = out;
29
+ } else if (out && typeof out === "object") {
30
+ toolOutput = out.stdout || out.content || JSON.stringify(out);
31
+ }
32
+ if (!toolOutput) process.exit(0);
33
+
34
+ // Detect Vercel deploy URL
35
+ let deployUrl = "";
36
+
37
+ // Priority 1: Vercel CLI "Preview:" or "Production:" line
38
+ const cliMatch = toolOutput.match(
39
+ /(?:Preview|Production): (https:\/\/[a-zA-Z0-9._-]+)/,
40
+ );
41
+ if (cliMatch) {
42
+ deployUrl = cliMatch[1];
43
+ }
44
+
45
+ // Priority 2: Any *.vercel.app URL
46
+ if (!deployUrl) {
47
+ const appMatch = toolOutput.match(
48
+ /https:\/\/[a-zA-Z0-9._-]+\.vercel\.app/,
49
+ );
50
+ if (appMatch) {
51
+ deployUrl = appMatch[0];
52
+ }
53
+ }
54
+
55
+ if (!deployUrl) process.exit(0);
56
+
57
+ // Deploy URL found — register the build
58
+ const cwd = input.cwd;
59
+ if (!cwd) process.exit(0);
60
+
61
+ const config = readMiraConfig(cwd);
62
+ if (!config) process.exit(0);
63
+
64
+ const credentials = readMcpCredentials(cwd);
65
+ if (!credentials) process.exit(0);
66
+
67
+ const git = getGitInfo(cwd);
68
+
69
+ const buildBody = { url: deployUrl, git_ref: git.ref, track: git.track };
70
+ if (git.commitMessage) {
71
+ buildBody.commit_message = git.commitMessage;
72
+ }
73
+
74
+ const result = await miraApi(
75
+ "POST",
76
+ `/api/v1/projects/${config.projectId}/builds`,
77
+ buildBody,
78
+ credentials,
79
+ );
80
+
81
+ if (result && result.httpCode >= 200 && result.httpCode < 300) {
82
+ const buildId = result.response?.id;
83
+ const reviewUrl = `${credentials.apiUrl}/review/${buildId}`;
84
+
85
+ console.log(
86
+ JSON.stringify({
87
+ reason: `Mira: Build auto-registered. Deploy: ${deployUrl} | Review: ${reviewUrl} | Git: ${git.ref} (${git.track}). Stakeholders can now leave feedback at the review URL.`,
88
+ }),
89
+ );
90
+ }
91
+ } catch {
92
+ // Hooks must never block Claude Code
93
+ process.exit(0);
94
+ }
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+
3
+ // SessionStart hook: surface pending feedback when opening a project.
4
+ //
5
+ // Fires at session start. Checks if the project directory has a .mira.json,
6
+ // and if so, queries the API for pending feedback and injects context so
7
+ // Claude can proactively inform the developer.
8
+
9
+ import { readStdin, readMiraConfig, readMcpCredentials, miraApi } from "./lib.js";
10
+
11
+ try {
12
+ const input = readStdin();
13
+ if (!input) process.exit(0);
14
+
15
+ const cwd = input.cwd;
16
+ if (!cwd) process.exit(0);
17
+
18
+ const config = readMiraConfig(cwd);
19
+ if (!config) process.exit(0);
20
+
21
+ const credentials = readMcpCredentials(cwd);
22
+ if (!credentials) process.exit(0);
23
+
24
+ const result = await miraApi(
25
+ "GET",
26
+ `/api/v1/projects/${config.projectId}/feedback/pending`,
27
+ null,
28
+ credentials,
29
+ );
30
+
31
+ if (!result || result.httpCode < 200 || result.httpCode >= 300) process.exit(0);
32
+
33
+ const pendingCount = result.response?.pending_count || 0;
34
+ if (pendingCount <= 0) process.exit(0);
35
+
36
+ // Build summary lines
37
+ const builds = result.response?.builds_with_pending || [];
38
+ const summaryLines = builds.map((b) => {
39
+ const date = (b.deployed_at || "").split("T")[0];
40
+ const roles = (b.roles || []).join(", ");
41
+ return ` - ${b.track} (deployed ${date}): ${b.pending_count} pending from ${roles}`;
42
+ });
43
+
44
+ const context = [
45
+ `Mira: ${pendingCount} pending feedback item(s) on this project:`,
46
+ ...summaryLines,
47
+ 'Use fetch_feedback to review, or say "mira, what feedback do we have?"',
48
+ ].join("\n");
49
+
50
+ console.log(
51
+ JSON.stringify({
52
+ hookSpecificOutput: {
53
+ additionalContext: context,
54
+ },
55
+ }),
56
+ );
57
+ } catch {
58
+ // Hooks must never block Claude Code
59
+ process.exit(0);
60
+ }
package/hooks/lib.js ADDED
@@ -0,0 +1,167 @@
1
+ import { readFileSync, existsSync } from "node:fs";
2
+ import { execSync } from "node:child_process";
3
+ import { join } from "node:path";
4
+ import { homedir } from "node:os";
5
+
6
+ /**
7
+ * Read and parse JSON from stdin (synchronous).
8
+ * Returns parsed object or null on failure.
9
+ */
10
+ export function readStdin() {
11
+ try {
12
+ const raw = readFileSync(0, "utf8");
13
+ return JSON.parse(raw);
14
+ } catch {
15
+ return null;
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Read .mira.json from a directory.
21
+ * Returns { projectId } or null if not found/invalid.
22
+ */
23
+ export function readMiraConfig(dir) {
24
+ try {
25
+ const configPath = join(dir, ".mira.json");
26
+ if (!existsSync(configPath)) return null;
27
+ const config = JSON.parse(readFileSync(configPath, "utf8"));
28
+ if (!config.projectId) return null;
29
+ return { projectId: config.projectId };
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Read MCP server credentials from Claude Code settings files.
37
+ * Checks project-level then user-level settings, and both "mira" and "cc-feedback" server names.
38
+ * Falls back to .mira.json for credentials.
39
+ * Returns { apiUrl, apiKey } or null.
40
+ */
41
+ export function readMcpCredentials(dir) {
42
+ const settingsFiles = [
43
+ join(dir, ".claude", "settings.local.json"),
44
+ join(dir, ".claude", "settings.json"),
45
+ join(homedir(), ".claude", "settings.json"),
46
+ ];
47
+ const serverNames = ["mira", "cc-feedback"];
48
+
49
+ let apiUrl = "";
50
+ let apiKey = "";
51
+
52
+ for (const sf of settingsFiles) {
53
+ if (apiKey) break;
54
+ try {
55
+ if (!existsSync(sf)) continue;
56
+ const settings = JSON.parse(readFileSync(sf, "utf8"));
57
+ for (const sn of serverNames) {
58
+ const env = settings?.mcpServers?.[sn]?.env;
59
+ if (env?.CC_FEEDBACK_API_KEY) {
60
+ apiUrl = env.CC_FEEDBACK_API_URL || "";
61
+ apiKey = env.CC_FEEDBACK_API_KEY;
62
+ break;
63
+ }
64
+ }
65
+ } catch {
66
+ continue;
67
+ }
68
+ }
69
+
70
+ // Fall back to .mira.json for credentials
71
+ if (!apiKey) {
72
+ try {
73
+ const configPath = join(dir, ".mira.json");
74
+ if (!existsSync(configPath)) return null;
75
+ const config = JSON.parse(readFileSync(configPath, "utf8"));
76
+ apiUrl = config.apiUrl || "";
77
+ apiKey = config.apiKey || "";
78
+ } catch {
79
+ return null;
80
+ }
81
+ }
82
+
83
+ if (!apiKey) return null;
84
+
85
+ return {
86
+ apiUrl: apiUrl || "https://okmira.ai",
87
+ apiKey,
88
+ };
89
+ }
90
+
91
+ /**
92
+ * Make an authenticated API request.
93
+ * Returns { httpCode, response } or null on failure.
94
+ */
95
+ export async function miraApi(method, path, body, credentials) {
96
+ try {
97
+ const url = `${credentials.apiUrl}${path}`;
98
+ const options = {
99
+ method,
100
+ headers: {
101
+ "Content-Type": "application/json",
102
+ Authorization: `Bearer ${credentials.apiKey}`,
103
+ },
104
+ signal: AbortSignal.timeout(10000),
105
+ };
106
+
107
+ if (body) {
108
+ options.body = JSON.stringify(body);
109
+ }
110
+
111
+ const res = await fetch(url, options);
112
+ const text = await res.text();
113
+ let response;
114
+ try {
115
+ response = JSON.parse(text);
116
+ } catch {
117
+ response = text;
118
+ }
119
+
120
+ return { httpCode: res.status, response };
121
+ } catch {
122
+ return null;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Extract git metadata from a directory.
128
+ * Returns { ref, track }.
129
+ */
130
+ export function getGitInfo(dir) {
131
+ let ref = "unknown";
132
+ let track = "unknown";
133
+
134
+ let commitMessage = "";
135
+
136
+ try {
137
+ ref = execSync("git rev-parse --short HEAD", {
138
+ cwd: dir,
139
+ encoding: "utf8",
140
+ stdio: ["pipe", "pipe", "pipe"],
141
+ }).trim();
142
+ } catch {
143
+ // ignore
144
+ }
145
+
146
+ try {
147
+ track = execSync("git branch --show-current", {
148
+ cwd: dir,
149
+ encoding: "utf8",
150
+ stdio: ["pipe", "pipe", "pipe"],
151
+ }).trim() || "unknown";
152
+ } catch {
153
+ // ignore
154
+ }
155
+
156
+ try {
157
+ commitMessage = execSync("git log -1 --format=%s", {
158
+ cwd: dir,
159
+ encoding: "utf8",
160
+ stdio: ["pipe", "pipe", "pipe"],
161
+ }).trim();
162
+ } catch {
163
+ // ignore
164
+ }
165
+
166
+ return { ref, track, commitMessage };
167
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@okmira/mcp",
3
+ "version": "0.0.1",
4
+ "description": "MCP server for Mira — structured stakeholder feedback for AI-assisted development",
5
+ "type": "module",
6
+ "bin": {
7
+ "okmira-mcp": "./dist/index.js",
8
+ "mira-auto-register": "./hooks/auto-register.js",
9
+ "mira-check-feedback": "./hooks/check-feedback.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "hooks"
14
+ ],
15
+ "scripts": {
16
+ "dev": "tsx watch src/index.ts",
17
+ "build": "tsc",
18
+ "start": "node dist/index.js",
19
+ "lint": "eslint . && tsc --noEmit",
20
+ "prepublishOnly": "tsc"
21
+ },
22
+ "dependencies": {
23
+ "@modelcontextprotocol/sdk": "^1.0.0",
24
+ "zod": "3"
25
+ },
26
+ "devDependencies": {
27
+ "@cc-feedback/shared": "workspace:*",
28
+ "@types/node": "^22.0.0",
29
+ "tsx": "^4.19.0",
30
+ "typescript": "^5.7.0"
31
+ }
32
+ }