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 +9 -0
- package/dist/index.js +59 -6
- package/dist/lib.js +28 -0
- package/package.json +1 -1
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.
|
|
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.
|
|
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
|
|
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) {
|