@scottsus/mdreview-mcp 0.1.0 → 0.4.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.
@@ -33,26 +33,25 @@ interface CommentResponse {
33
33
  authorName: string | null;
34
34
  createdAt: string;
35
35
  }
36
- interface WaitResponse {
36
+ interface SubmitDecisionResponse {
37
37
  id: string;
38
38
  status: string;
39
39
  decisionMessage: string | null;
40
40
  decidedAt: string | null;
41
- threads: ThreadResponse[];
42
- summary: {
43
- totalThreads: number;
44
- resolvedThreads: number;
45
- unresolvedThreads: number;
46
- totalComments: number;
47
- };
41
+ }
42
+ interface ResolveThreadResponse {
43
+ id: string;
44
+ resolved: boolean;
45
+ resolvedAt: string | null;
48
46
  }
49
47
  export declare class ApiClient {
50
48
  private baseUrl;
51
49
  constructor(baseUrl?: string);
52
- createReview(content: string, title?: string): Promise<CreateReviewResponse>;
53
- getReview(reviewId: string): Promise<ReviewResponse>;
54
- waitForReview(reviewId: string, timeoutSeconds?: number): Promise<WaitResponse>;
50
+ createReview(content: string): Promise<CreateReviewResponse>;
51
+ getReview(reviewId: string, includeContent?: boolean): Promise<ReviewResponse>;
55
52
  addComment(threadId: string, body: string): Promise<CommentResponse>;
53
+ submitDecision(reviewId: string, decision: "approved" | "rejected" | "changes_requested", message?: string): Promise<SubmitDecisionResponse>;
54
+ resolveThread(threadId: string): Promise<ResolveThreadResponse>;
56
55
  }
57
56
  export {};
58
57
  //# sourceMappingURL=api-client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"AAEA,UAAU,oBAAoB;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,cAAc;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,cAAc;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC7B;AAED,UAAU,eAAe;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,YAAY;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,OAAO,EAAE;QACP,YAAY,EAAE,MAAM,CAAC;QACrB,eAAe,EAAE,MAAM,CAAC;QACxB,iBAAiB,EAAE,MAAM,CAAC;QAC1B,aAAa,EAAE,MAAM,CAAC;KACvB,CAAC;CACH;AAED,qBAAa,SAAS;IACpB,OAAO,CAAC,OAAO,CAAS;gBAEZ,OAAO,CAAC,EAAE,MAAM;IAItB,YAAY,CAChB,OAAO,EAAE,MAAM,EACf,KAAK,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,oBAAoB,CAAC;IAmB1B,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAWpD,aAAa,CACjB,QAAQ,EAAE,MAAM,EAChB,cAAc,SAAM,GACnB,OAAO,CAAC,YAAY,CAAC;IAalB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;CAqB3E"}
1
+ {"version":3,"file":"api-client.d.ts","sourceRoot":"","sources":["../src/api-client.ts"],"names":[],"mappings":"AAEA,UAAU,oBAAoB;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,cAAc;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,cAAc;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,eAAe,EAAE,CAAC;CAC7B;AAED,UAAU,eAAe;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,sBAAsB;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,UAAU,qBAAqB;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,qBAAa,SAAS;IACpB,OAAO,CAAC,OAAO,CAAS;gBAEZ,OAAO,CAAC,EAAE,MAAM;IAItB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC;IAkB5D,SAAS,CACb,QAAQ,EAAE,MAAM,EAChB,cAAc,UAAQ,GACrB,OAAO,CAAC,cAAc,CAAC;IAcpB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAsBpE,cAAc,CAClB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,UAAU,GAAG,UAAU,GAAG,mBAAmB,EACvD,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,sBAAsB,CAAC;IAkB5B,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC;CActE"}
@@ -4,13 +4,12 @@ export class ApiClient {
4
4
  constructor(baseUrl) {
5
5
  this.baseUrl = baseUrl || BASE_URL;
6
6
  }
7
- async createReview(content, title) {
7
+ async createReview(content) {
8
8
  const response = await fetch(`${this.baseUrl}/api/reviews`, {
9
9
  method: "POST",
10
10
  headers: { "Content-Type": "application/json" },
11
11
  body: JSON.stringify({
12
12
  content,
13
- title,
14
13
  source: "agent",
15
14
  }),
16
15
  });
@@ -20,22 +19,17 @@ export class ApiClient {
20
19
  }
21
20
  return response.json();
22
21
  }
23
- async getReview(reviewId) {
24
- const response = await fetch(`${this.baseUrl}/api/reviews/${reviewId}`);
22
+ async getReview(reviewId, includeContent = false) {
23
+ const url = includeContent
24
+ ? `${this.baseUrl}/api/reviews/${reviewId}?includeContent=true`
25
+ : `${this.baseUrl}/api/reviews/${reviewId}`;
26
+ const response = await fetch(url);
25
27
  if (!response.ok) {
26
28
  const error = await response.json();
27
29
  throw new Error(error.message || "Failed to get review");
28
30
  }
29
31
  return response.json();
30
32
  }
31
- async waitForReview(reviewId, timeoutSeconds = 300) {
32
- const response = await fetch(`${this.baseUrl}/api/reviews/${reviewId}/wait?timeout=${timeoutSeconds}`);
33
- if (!response.ok) {
34
- const error = await response.json();
35
- throw new Error(error.message || "Failed to wait for review");
36
- }
37
- return response.json();
38
- }
39
33
  async addComment(threadId, body) {
40
34
  const response = await fetch(`${this.baseUrl}/api/threads/${threadId}/replies`, {
41
35
  method: "POST",
@@ -52,4 +46,28 @@ export class ApiClient {
52
46
  }
53
47
  return response.json();
54
48
  }
49
+ async submitDecision(reviewId, decision, message) {
50
+ const response = await fetch(`${this.baseUrl}/api/reviews/${reviewId}/submit`, {
51
+ method: "POST",
52
+ headers: { "Content-Type": "application/json" },
53
+ body: JSON.stringify({ decision, message }),
54
+ });
55
+ if (!response.ok) {
56
+ const error = await response.json();
57
+ throw new Error(error.message || "Failed to submit decision");
58
+ }
59
+ return response.json();
60
+ }
61
+ async resolveThread(threadId) {
62
+ const response = await fetch(`${this.baseUrl}/api/threads/${threadId}`, {
63
+ method: "PATCH",
64
+ headers: { "Content-Type": "application/json" },
65
+ body: JSON.stringify({ resolved: true }),
66
+ });
67
+ if (!response.ok) {
68
+ const error = await response.json();
69
+ throw new Error(error.message || "Failed to resolve thread");
70
+ }
71
+ return response.json();
72
+ }
55
73
  }
package/dist/index.js CHANGED
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env node
2
+ import fs from "fs";
2
3
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
5
  import { z } from "zod";
5
6
  import { ApiClient } from "./api-client.js";
6
7
  const server = new McpServer({
7
8
  name: "mdreview",
8
- version: "0.1.0",
9
+ version: "0.4.0",
9
10
  });
10
11
  const apiClient = new ApiClient(process.env.MDREVIEW_BASE_URL);
11
12
  // Tool: request_review
@@ -13,67 +14,38 @@ server.registerTool("request_review", {
13
14
  title: "Request Review",
14
15
  description: "Create a markdown document review and get a shareable URL. Share this URL with the reviewer and they can add inline comments and approve/reject the document.",
15
16
  inputSchema: {
16
- content: z.string().describe("The markdown content to review"),
17
- title: z.string().optional().describe("Optional title for the review"),
17
+ filePath: z.string().describe("The path to the markdown file to review"),
18
18
  },
19
- }, async ({ content, title }) => {
20
- const result = await apiClient.createReview(content, title);
21
- return {
22
- content: [
23
- {
24
- type: "text",
25
- text: `Review created successfully!\n\nReview URL: ${result.url}\n\nShare this URL with your reviewer. They can:\n- Add inline comments by selecting text\n- Approve, reject, or request changes\n\nUse 'wait_for_review' with reviewId "${result.id}" to wait for the review to complete.`,
26
- },
27
- ],
28
- structuredContent: {
29
- reviewId: result.id,
30
- url: result.url,
31
- status: result.status,
32
- },
33
- };
34
- });
35
- // Tool: wait_for_review
36
- server.registerTool("wait_for_review", {
37
- title: "Wait for Review",
38
- description: "Wait for a review to be completed (approved, rejected, or changes requested). This will block until the reviewer makes a decision or the timeout is reached.",
39
- inputSchema: {
40
- reviewId: z.string().describe("The review ID to wait for"),
41
- timeoutSeconds: z
42
- .number()
43
- .min(1)
44
- .max(300)
45
- .default(300)
46
- .describe("Maximum time to wait in seconds (default: 300)"),
47
- },
48
- }, async ({ reviewId, timeoutSeconds }) => {
49
- const result = await apiClient.waitForReview(reviewId, timeoutSeconds);
50
- if (result.status === "pending") {
19
+ }, async ({ filePath }) => {
20
+ let content;
21
+ try {
22
+ content = fs.readFileSync(filePath, "utf-8");
23
+ }
24
+ catch (error) {
25
+ const message = error instanceof Error ? error.message : "Unknown error reading file";
51
26
  return {
52
27
  content: [
53
28
  {
54
29
  type: "text",
55
- text: `Review is still pending after ${timeoutSeconds} seconds. You can call wait_for_review again to continue waiting.`,
30
+ text: `Error: Failed to read file at "${filePath}"\n\n${message}`,
56
31
  },
57
32
  ],
58
- structuredContent: { status: "pending", timedOut: true },
33
+ isError: true,
59
34
  };
60
35
  }
61
- const commentsText = result.threads
62
- .map((thread) => {
63
- const comments = thread.comments
64
- .map((c) => ` - ${c.authorType}: ${c.body}`)
65
- .join("\n");
66
- return `[${thread.resolved ? "RESOLVED" : "UNRESOLVED"}] "${thread.selectedText}"\n${comments}`;
67
- })
68
- .join("\n\n");
36
+ const result = await apiClient.createReview(content);
69
37
  return {
70
38
  content: [
71
39
  {
72
40
  type: "text",
73
- text: `Review completed!\n\nStatus: ${result.status.toUpperCase()}\nMessage: ${result.decisionMessage || "(none)"}\n\nSummary:\n- Total threads: ${result.summary.totalThreads}\n- Resolved: ${result.summary.resolvedThreads}\n- Unresolved: ${result.summary.unresolvedThreads}\n- Total comments: ${result.summary.totalComments}\n\n${commentsText ? `Comments:\n${commentsText}` : "No comments."}`,
41
+ text: `Review created successfully!\n\nReview URL: ${result.url}\n\nShare this URL with your reviewer. They can:\n- Add inline comments by selecting text\n- Approve, reject, or request changes\n\nUse 'get_review_status' with reviewId "${result.id}" to check the review status.`,
74
42
  },
75
43
  ],
76
- structuredContent: { ...result },
44
+ structuredContent: {
45
+ reviewId: result.id,
46
+ url: result.url,
47
+ status: result.status,
48
+ },
77
49
  };
78
50
  });
79
51
  // Tool: get_review_status
@@ -82,21 +54,45 @@ server.registerTool("get_review_status", {
82
54
  description: "Get the current status of a review without waiting. Use this to check if a review has been completed.",
83
55
  inputSchema: {
84
56
  reviewId: z.string().describe("The review ID to check"),
57
+ includeContent: z
58
+ .boolean()
59
+ .default(false)
60
+ .describe("Include the full markdown content in the response"),
85
61
  },
86
- }, async ({ reviewId }) => {
87
- const result = await apiClient.getReview(reviewId);
62
+ }, async ({ reviewId, includeContent }) => {
63
+ const result = await apiClient.getReview(reviewId, includeContent);
64
+ const commentsText = result.threads
65
+ .map((thread) => {
66
+ const comments = thread.comments
67
+ .map((c) => ` - ${c.authorType}: ${c.body}`)
68
+ .join("\n");
69
+ return `[${thread.resolved ? "RESOLVED" : "UNRESOLVED"}] Thread: ${thread.id}\n"${thread.selectedText}"\n${comments}`;
70
+ })
71
+ .join("\n\n");
72
+ const summary = {
73
+ totalThreads: result.threads.length,
74
+ resolvedThreads: result.threads.filter((t) => t.resolved).length,
75
+ unresolvedThreads: result.threads.filter((t) => !t.resolved).length,
76
+ totalComments: result.threads.reduce((sum, t) => sum + t.comments.length, 0),
77
+ };
78
+ const structuredContent = {
79
+ status: result.status,
80
+ decisionMessage: result.decisionMessage,
81
+ decidedAt: result.decidedAt,
82
+ threads: result.threads,
83
+ summary,
84
+ };
85
+ if (includeContent) {
86
+ structuredContent.content = result.content;
87
+ }
88
88
  return {
89
89
  content: [
90
90
  {
91
91
  type: "text",
92
- text: `Review Status: ${result.status.toUpperCase()}\nTitle: ${result.title || "(untitled)"}\nThreads: ${result.threads.length}\nURL: ${result.url}`,
92
+ text: `Review Status: ${result.status.toUpperCase()}\nTitle: ${result.title || "(untitled)"}\nMessage: ${result.decisionMessage || "(none)"}\nURL: ${result.url}\n\nSummary:\n- Total threads: ${summary.totalThreads}\n- Resolved: ${summary.resolvedThreads}\n- Unresolved: ${summary.unresolvedThreads}\n- Total comments: ${summary.totalComments}\n\n${commentsText ? `Comments:\n${commentsText}` : "No comments."}`,
93
93
  },
94
94
  ],
95
- structuredContent: {
96
- status: result.status,
97
- threadCount: result.threads.length,
98
- commentCount: result.threads.reduce((sum, t) => sum + t.comments.length, 0),
99
- },
95
+ structuredContent,
100
96
  };
101
97
  });
102
98
  // Tool: add_comment
@@ -122,6 +118,60 @@ server.registerTool("add_comment", {
122
118
  },
123
119
  };
124
120
  });
121
+ // Tool: resolve_thread
122
+ server.registerTool("resolve_thread", {
123
+ title: "Resolve Thread",
124
+ description: "Mark a comment thread as resolved. Use this after addressing reviewer feedback.",
125
+ inputSchema: {
126
+ threadId: z.string().describe("The thread ID to resolve"),
127
+ },
128
+ }, async ({ threadId }) => {
129
+ const result = await apiClient.resolveThread(threadId);
130
+ return {
131
+ content: [
132
+ {
133
+ type: "text",
134
+ text: `Thread resolved successfully!\n\nThread ID: ${result.id}\nResolved at: ${result.resolvedAt}`,
135
+ },
136
+ ],
137
+ structuredContent: {
138
+ threadId: result.id,
139
+ resolved: result.resolved,
140
+ resolvedAt: result.resolvedAt,
141
+ },
142
+ };
143
+ });
144
+ // Tool: submit_decision
145
+ server.registerTool("submit_decision", {
146
+ title: "Submit Decision",
147
+ description: "Submit a review decision (approve, reject, or request changes). Use this to finalize a review.",
148
+ inputSchema: {
149
+ reviewId: z.string().describe("The review ID"),
150
+ decision: z
151
+ .enum(["approved", "rejected", "changes_requested"])
152
+ .describe("The decision"),
153
+ message: z
154
+ .string()
155
+ .optional()
156
+ .describe("Optional message explaining the decision"),
157
+ },
158
+ }, async ({ reviewId, decision, message }) => {
159
+ const result = await apiClient.submitDecision(reviewId, decision, message);
160
+ return {
161
+ content: [
162
+ {
163
+ type: "text",
164
+ text: `Decision submitted successfully!\n\nReview ID: ${result.id}\nDecision: ${result.status.toUpperCase()}\nMessage: ${result.decisionMessage || "(none)"}`,
165
+ },
166
+ ],
167
+ structuredContent: {
168
+ reviewId: result.id,
169
+ status: result.status,
170
+ decisionMessage: result.decisionMessage,
171
+ decidedAt: result.decidedAt,
172
+ },
173
+ };
174
+ });
125
175
  async function main() {
126
176
  const transport = new StdioServerTransport();
127
177
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scottsus/mdreview-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.4.0",
4
4
  "description": "MCP server for MDReview - markdown document review tool",
5
5
  "type": "module",
6
6
  "bin": {