aixischeck-mcp 0.3.0 → 0.5.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
@@ -21,6 +21,15 @@ upload. Generate an HTML one-pager in chat, and it lands in your workspace, rend
21
21
  (base_version derived from the request); records what was done with each comment
22
22
  (incorporated / partial / skipped / not_addressed). The write-back to Postgres is one
23
23
  atomic RPC (concurrency + idempotency guaranteed).
24
+ - **`create_comments`** — `{ doc_id, origin, items[] }`. Bulk-creates review comments,
25
+ e.g. from a transcript of a reviewer narrating feedback while reading (such as a
26
+ Granola recording). Each item needs `section_index` (0-based, from `get_doc`'s
27
+ `sections`) and an EXACT verbatim `selected_text` quote from that section —
28
+ paraphrased quotes are rejected, not force-matched. `origin` is `'ai-transcribed'`
29
+ (matched from a human transcript) or `'ai-generated'`. Up to 20 items per call;
30
+ each item succeeds or fails independently (`created`/`rejected`, with a reason).
31
+ Every created comment lands **unconfirmed** — the reviewer must confirm it in the
32
+ AIxisCheck sidebar before it counts as a real review comment.
24
33
 
25
34
  ## Setup
26
35
 
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@
12
12
  // AIXIS_URL — Supabase project URL, e.g. https://<ref>.supabase.co
13
13
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14
14
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
- import { buildCreateDocBody, createDocShape, edgeErrorMessage, functionUrl, getDocShape, listCommentsShape, listDocsShape, updateDocShape, } from "./lib.js";
15
+ import { buildCreateDocBody, createCommentsShape, createDocShape, createRevisionRequestShape, edgeErrorMessage, functionUrl, getDocShape, listCommentsShape, listDocsShape, updateDocShape, } from "./lib.js";
16
16
  const AIXIS_TOKEN = process.env.AIXIS_TOKEN ?? "";
17
17
  const AIXIS_URL = process.env.AIXIS_URL ?? "";
18
18
  if (!AIXIS_TOKEN || !AIXIS_URL) {
@@ -43,7 +43,7 @@ function errorResult(e) {
43
43
  const message = e instanceof Error ? e.message : String(e);
44
44
  return { content: [{ type: "text", text: message }], isError: true };
45
45
  }
46
- const server = new McpServer({ name: "aixischeck-mcp", version: "0.3.0" });
46
+ const server = new McpServer({ name: "aixischeck-mcp", version: "0.5.0" });
47
47
  server.registerTool("list_workspaces", {
48
48
  description: "List the AIxisCheck workspaces you can write to. Returns each workspace's id, name, and your role. Call this first to get a workspace_id for create_doc.",
49
49
  inputSchema: {},
@@ -57,8 +57,13 @@ server.registerTool("list_workspaces", {
57
57
  return errorResult(e);
58
58
  }
59
59
  });
60
+ // create_doc makes a BRAND-NEW doc. It is NOT how you revise an existing one:
61
+ // to address comments on a doc the user pointed you at, use
62
+ // create_revision_request → update_doc (which lands a new VERSION of that same
63
+ // doc, preserving its history, comments, and share URL). create_doc on a revision
64
+ // would orphan a detached copy. Keep these two paths distinct.
60
65
  server.registerTool("create_doc", {
61
- description: "Create a document in an AIxisCheck workspace from generated content. Defaults to an HTML prototype (doc_type='html'); pass doc_type='markdown' for a markdown doc. Pass a short, human-readable `title` (e.g. \"Q3 Launch One-Pager\"), not a filename. Use list_workspaces first to get a workspace_id. Returns the URL of the new doc.",
66
+ description: "Create a NEW document in an AIxisCheck workspace from generated content. Defaults to an HTML prototype (doc_type='html'); pass doc_type='markdown' for a markdown doc. Pass a short, human-readable `title` (e.g. \"Q3 Launch One-Pager\"), not a filename. Use list_workspaces first to get a workspace_id. Returns the URL of the new doc. NOTE: to revise an EXISTING doc (e.g. address its comments), do not use this — use create_revision_request then update_doc.",
62
67
  inputSchema: createDocShape,
63
68
  }, async (args) => {
64
69
  try {
@@ -84,7 +89,7 @@ server.registerTool("list_docs", {
84
89
  }
85
90
  });
86
91
  server.registerTool("get_doc", {
87
- description: "Read one AIxisCheck document: its content (HTML or markdown), doc_status, current_version, and section list. Pass the doc_id from list_docs. current_version is the value to echo as base_version when revising via update_doc.",
92
+ description: "Read one AIxisCheck document: its content (HTML or markdown), doc_status, current_version, and section list. Pass the doc_id from list_docs (or parse it from a doc URL like /web/#/review/<doc_id>). To SAVE a revision you need a request_id; if you don't have one (e.g. the user only gave you a URL and asked you to address comments), call create_revision_request first.",
88
93
  inputSchema: getDocShape,
89
94
  }, async (args) => {
90
95
  try {
@@ -96,7 +101,7 @@ server.registerTool("get_doc", {
96
101
  }
97
102
  });
98
103
  server.registerTool("list_comments", {
99
- description: "List the review comments on an AIxisCheck document. Pass filter:'open' for just the active (non-archived) comments to address; default returns { active, archived }. Each comment has id, selectedText, and note.",
104
+ description: "List the review comments on an AIxisCheck document. Pass filter:'open' for just the active (non-archived) comments to address; default returns { active, archived }. Each comment has id, selectedText, note, and source. source:'member' comments come from a workspace member reviewing in the app; source:'guest' comments come from an external reviewer on the doc's public share link (these also carry authorName and publishedSlug). Only approved guest comments appear. Treat guest feedback as you would member feedback when revising.",
100
105
  inputSchema: listCommentsShape,
101
106
  }, async (args) => {
102
107
  try {
@@ -107,8 +112,30 @@ server.registerTool("list_comments", {
107
112
  return errorResult(e);
108
113
  }
109
114
  });
115
+ server.registerTool("create_revision_request", {
116
+ description: "Mint a revision request for a doc the user pointed you at (e.g. by URL) so you can save your revision with update_doc. Pass the doc_id. Snapshots the doc's CURRENT open comments (member + approved guest — the same set list_comments returns) at the doc's current version, and returns a request_id. Idempotent per (doc, version): calling it again before you revise returns the SAME request_id. Use this when you have no request_id from a 'Send to Claude' prompt. Owner/editor only.",
117
+ inputSchema: createRevisionRequestShape,
118
+ }, async (args) => {
119
+ try {
120
+ const data = await callEdge("mcp-create-revision", "POST", { doc_id: args.doc_id });
121
+ const id = data?.request_id;
122
+ const n = data?.comment_count;
123
+ if (!id)
124
+ throw new Error("Revision request creation returned an unexpected response.");
125
+ return {
126
+ content: [{
127
+ type: "text",
128
+ text: `Revision request ${id} created for ${n} open comment(s). Revise the doc, then call ` +
129
+ `update_doc with request_id "${id}" and a comment_dispositions entry for each comment.`,
130
+ }],
131
+ };
132
+ }
133
+ catch (e) {
134
+ return errorResult(e);
135
+ }
136
+ });
110
137
  server.registerTool("update_doc", {
111
- description: "Save a revised version of an AIxisCheck doc that addresses a revision request's comments. Pass: doc_id; content (the FULL revised doc); request_id (from the user's 'Send to Claude' prompt); and comment_dispositions recording what you did with each comment (outcome: incorporated | partial | skipped | not_addressed, plus a short rationale). The revision lands as a NEW version (base_version is derived from the request). Returns the new version number. If you get a conflict, call get_doc again and retry.",
138
+ description: "Save a revised version of an AIxisCheck doc that addresses a revision request's comments. Pass: doc_id; content (the FULL revised doc); request_id (from the user's 'Send to Claude' prompt, OR from create_revision_request if you started from a URL); and comment_dispositions recording what you did with each comment (outcome: incorporated | partial | skipped | not_addressed, plus a short rationale). The revision lands as a NEW version (base_version is derived from the request). Returns the new version number. If you get a conflict, call get_doc again and retry.",
112
139
  inputSchema: updateDocShape,
113
140
  }, async (args) => {
114
141
  try {
@@ -130,5 +157,31 @@ server.registerTool("update_doc", {
130
157
  return errorResult(e);
131
158
  }
132
159
  });
160
+ server.registerTool("create_comments", {
161
+ description: "Bulk-create review comments on an AIxisCheck doc — e.g. from a transcript of a reviewer narrating feedback while reading (such as a Granola recording). Call get_doc FIRST to read the doc's actual content and section list: each item's selected_text must be an EXACT, verbatim substring of the target section — do not paraphrase. Set origin:'ai-transcribed' when the feedback came from a human transcript, or 'ai-generated' otherwise. Up to 20 items per call. Items that don't match verbatim (or match ambiguously — the same phrase appears more than once in that section) are rejected individually; the rest of the batch still succeeds. Every created comment lands UNCONFIRMED in the sidebar — the reviewer must confirm it there before it counts as a real review comment.",
162
+ inputSchema: createCommentsShape,
163
+ }, async (args) => {
164
+ try {
165
+ const data = await callEdge("mcp-create-comments", "POST", {
166
+ doc_id: args.doc_id,
167
+ origin: args.origin,
168
+ items: args.items,
169
+ });
170
+ const created = Array.isArray(data?.created) ? data.created : [];
171
+ const rejected = Array.isArray(data?.rejected) ? data.rejected : [];
172
+ const parts = [`Created ${created.length} comment(s).`];
173
+ if (rejected.length) {
174
+ const detail = rejected
175
+ .map((r) => `item ${r.item_index} (${r.reason})`)
176
+ .join(", ");
177
+ parts.push(`${rejected.length} rejected: ${detail}. Re-check each quote against the section's actual ` +
178
+ "text (via get_doc) and retry those items if needed.");
179
+ }
180
+ return { content: [{ type: "text", text: parts.join(" ") }] };
181
+ }
182
+ catch (e) {
183
+ return errorResult(e);
184
+ }
185
+ });
133
186
  const transport = new StdioServerTransport();
134
187
  await server.connect(transport);
package/dist/lib.js CHANGED
@@ -40,6 +40,34 @@ export const updateDocShape = {
40
40
  rationale: z.string().optional(),
41
41
  })).optional().describe("What you did with each comment you addressed. Any requested comment you omit is recorded as not_addressed."),
42
42
  };
43
+ // create_revision_request: mint (or reuse) a pending revision request for a doc
44
+ // the user pointed at by URL, so update_doc has a request_id to commit against.
45
+ // Snapshots the doc's current open comments server-side; the caller passes only
46
+ // the doc_id.
47
+ export const createRevisionRequestShape = {
48
+ doc_id: z.string().min(1, "doc_id is required"),
49
+ };
50
+ // create_comments (bulk comment import, 2026-07-09): write a batch of
51
+ // AI-matched comments onto a doc's existing sections. section_index is the
52
+ // 0-based index into get_doc's `sections` array (its sections[].id is this
53
+ // same index, stringified) — NOT a free-form id. selected_text must be an
54
+ // EXACT substring of that section's content; the edge function rejects
55
+ // items that don't match verbatim (or match ambiguously) rather than
56
+ // guessing. Every item in one call shares an `origin`, which becomes the
57
+ // created annotation's `source` and determines nothing else server-side —
58
+ // all AI-created comments land unconfirmed regardless of origin.
59
+ export const createCommentsShape = {
60
+ doc_id: z.string().min(1, "doc_id is required"),
61
+ origin: z.enum(["ai-transcribed", "ai-generated"]).describe("'ai-transcribed' if these comments were matched from a human transcript (e.g. a Granola recording of the reviewer narrating feedback while reading); 'ai-generated' if there was no human transcript source."),
62
+ items: z.array(z.object({
63
+ section_index: z.number().int().min(0).describe("0-based index into the sections array returned by get_doc (sections[].id is this same index, stringified)."),
64
+ selected_text: z.string().min(1).describe("An EXACT, verbatim substring of that section's content — read the doc with get_doc first; do not paraphrase."),
65
+ prefix_context: z.string().optional().describe("A few characters immediately before selected_text, to disambiguate if the phrase repeats in the section."),
66
+ suffix_context: z.string().optional().describe("A few characters immediately after selected_text, to disambiguate if the phrase repeats in the section."),
67
+ note: z.string().min(1, "note is required"),
68
+ })).min(1).max(20, "at most 20 comments per call — split larger batches into multiple calls"),
69
+ };
70
+ export const CreateCommentsSchema = z.object(createCommentsShape);
43
71
  // Shape the POST body for the mcp-create-doc edge function, applying
44
72
  // the doc_type='html' default and normalizing an absent title to ''.
45
73
  export function buildCreateDocBody(input) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aixischeck-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "MCP server for AIxisCheck — create docs in your workspace from Claude Desktop.",
5
5
  "license": "MIT",
6
6
  "type": "module",