@limeadelabs/clarabit-mcp 2.1.0 → 2.3.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/dist/index.js +243 -6
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -99,9 +99,10 @@ var ClarabitClient = class {
|
|
|
99
99
|
createPage(projectId, title, body) {
|
|
100
100
|
return this.request("POST", `/projects/${projectId}/pages`, { page: { title, body } });
|
|
101
101
|
}
|
|
102
|
-
updatePage(projectId, pageId, updates, changeSummary) {
|
|
102
|
+
updatePage(projectId, pageId, updates, changeSummary, verbose) {
|
|
103
103
|
const body = { page: updates };
|
|
104
104
|
if (changeSummary !== void 0) body.change_summary = changeSummary;
|
|
105
|
+
if (verbose !== void 0) body.verbose = verbose;
|
|
105
106
|
return this.request("PATCH", `/projects/${projectId}/pages/${pageId}`, body);
|
|
106
107
|
}
|
|
107
108
|
listPageVersions(projectId, pageId) {
|
|
@@ -130,9 +131,10 @@ var ClarabitClient = class {
|
|
|
130
131
|
getPageFlat(pageId) {
|
|
131
132
|
return this.request("GET", `/pages/${pageId}`);
|
|
132
133
|
}
|
|
133
|
-
updatePageFlat(pageId, updates, changeSummary) {
|
|
134
|
+
updatePageFlat(pageId, updates, changeSummary, verbose) {
|
|
134
135
|
const body = { page: updates };
|
|
135
136
|
if (changeSummary !== void 0) body.change_summary = changeSummary;
|
|
137
|
+
if (verbose !== void 0) body.verbose = verbose;
|
|
136
138
|
return this.request("PATCH", `/pages/${pageId}`, body);
|
|
137
139
|
}
|
|
138
140
|
listPageVersionsFlat(pageId) {
|
|
@@ -159,6 +161,13 @@ var ClarabitClient = class {
|
|
|
159
161
|
`/projects/${projectId}/pages/${pageId}/comments${query}`
|
|
160
162
|
);
|
|
161
163
|
}
|
|
164
|
+
listPageCommentsFlat(pageId, opts) {
|
|
165
|
+
const query = opts?.resolved === void 0 ? "" : `?resolved=${opts.resolved ? "true" : "false"}`;
|
|
166
|
+
return this.request(
|
|
167
|
+
"GET",
|
|
168
|
+
`/pages/${pageId}/comments${query}`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
162
171
|
createPageComment(projectId, pageId, body, opts) {
|
|
163
172
|
return this.request(
|
|
164
173
|
"POST",
|
|
@@ -551,20 +560,21 @@ function registerPageTools(server2, client2) {
|
|
|
551
560
|
);
|
|
552
561
|
server2.tool(
|
|
553
562
|
"cla_update_page",
|
|
554
|
-
"Update an existing spec/doc page in a Clarabit project. Every MCP edit creates a new PageVersion (no debounce). Optionally pass change_summary to label the snapshot.",
|
|
563
|
+
"Update an existing spec/doc page in a Clarabit project. Every MCP edit creates a new PageVersion (no debounce). Optionally pass change_summary to label the snapshot. Returns a minimal confirmation by default (id, title, version_number, change_summary) \u2014 pass verbose: true if you actually need the full body back (rare; the agent typically just sent it).",
|
|
555
564
|
{
|
|
556
565
|
project_id: z7.coerce.number().optional().describe("Project ID (optional \u2014 inferred if omitted)"),
|
|
557
566
|
page_id: z7.coerce.number().describe("Page ID"),
|
|
558
567
|
title: z7.string().optional().describe("New page title (optional)"),
|
|
559
568
|
body: z7.string().optional().describe("New page body in markdown (optional)"),
|
|
560
|
-
change_summary: z7.string().optional().describe("Short note describing why this edit was made (shown in version history)")
|
|
569
|
+
change_summary: z7.string().optional().describe("Short note describing why this edit was made (shown in version history)"),
|
|
570
|
+
verbose: z7.boolean().optional().describe("Return the full page (including body) in the response. Default false \u2014 saves round-trip cost on long pages.")
|
|
561
571
|
},
|
|
562
572
|
async (params) => {
|
|
563
573
|
try {
|
|
564
574
|
const updates = {};
|
|
565
575
|
if (params.title) updates.title = params.title;
|
|
566
576
|
if (params.body) updates.body = params.body;
|
|
567
|
-
const result = params.project_id ? await client2.updatePage(params.project_id, params.page_id, updates, params.change_summary) : await client2.updatePageFlat(params.page_id, updates, params.change_summary);
|
|
577
|
+
const result = params.project_id ? await client2.updatePage(params.project_id, params.page_id, updates, params.change_summary, params.verbose) : await client2.updatePageFlat(params.page_id, updates, params.change_summary, params.verbose);
|
|
568
578
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
569
579
|
} catch (error) {
|
|
570
580
|
return handleError(error, client2.timeoutMs);
|
|
@@ -650,10 +660,237 @@ function registerPageTools(server2, client2) {
|
|
|
650
660
|
|
|
651
661
|
// src/tools/page-comments.ts
|
|
652
662
|
import { z as z8 } from "zod";
|
|
663
|
+
|
|
664
|
+
// src/tools/anchor.ts
|
|
665
|
+
var PREVIEW = 40;
|
|
666
|
+
function computeAnchor(body, versionNumber, input) {
|
|
667
|
+
if (input.prefix !== void 0 && input.text !== void 0 && input.suffix !== void 0) {
|
|
668
|
+
return anchorWithContext(body, versionNumber, input.prefix, input.text, input.suffix);
|
|
669
|
+
}
|
|
670
|
+
if (input.heading !== void 0 && input.text !== void 0) {
|
|
671
|
+
return anchorInSection(body, versionNumber, input.heading, input.text);
|
|
672
|
+
}
|
|
673
|
+
if (input.exact_text !== void 0) {
|
|
674
|
+
return anchorExact(body, versionNumber, input.exact_text);
|
|
675
|
+
}
|
|
676
|
+
return {
|
|
677
|
+
error: "invalid_input",
|
|
678
|
+
hint: "Provide one of: exact_text, prefix+text+suffix, or heading+text"
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
function anchorExact(body, version, text) {
|
|
682
|
+
if (text.length === 0) {
|
|
683
|
+
return { error: "invalid_input", hint: "exact_text must be non-empty" };
|
|
684
|
+
}
|
|
685
|
+
const indices = findAll(body, text);
|
|
686
|
+
if (indices.length === 0) {
|
|
687
|
+
return { error: "no_match", hint: `exact_text not found in page body` };
|
|
688
|
+
}
|
|
689
|
+
if (indices.length > 1) {
|
|
690
|
+
return {
|
|
691
|
+
error: "ambiguous_anchor",
|
|
692
|
+
match_count: indices.length,
|
|
693
|
+
matches: indices.slice(0, 10).map((start) => ({
|
|
694
|
+
anchor_start: start,
|
|
695
|
+
anchor_end: start + text.length,
|
|
696
|
+
preview_before: body.slice(Math.max(0, start - PREVIEW), start),
|
|
697
|
+
preview_after: body.slice(
|
|
698
|
+
start + text.length,
|
|
699
|
+
Math.min(body.length, start + text.length + PREVIEW)
|
|
700
|
+
)
|
|
701
|
+
})),
|
|
702
|
+
hint: "Provide prefix/suffix or heading context to disambiguate."
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
return success(body, text, indices[0], version, "medium");
|
|
706
|
+
}
|
|
707
|
+
function anchorWithContext(body, version, prefix, text, suffix) {
|
|
708
|
+
if (text.length === 0) {
|
|
709
|
+
return { error: "invalid_input", hint: "text must be non-empty" };
|
|
710
|
+
}
|
|
711
|
+
const composite = prefix + text + suffix;
|
|
712
|
+
const indices = findAll(body, composite);
|
|
713
|
+
if (indices.length === 0) {
|
|
714
|
+
return {
|
|
715
|
+
error: "no_match",
|
|
716
|
+
hint: "prefix+text+suffix not found verbatim \u2014 try a shorter / different context"
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
if (indices.length > 1) {
|
|
720
|
+
return {
|
|
721
|
+
error: "ambiguous_anchor",
|
|
722
|
+
match_count: indices.length,
|
|
723
|
+
matches: indices.slice(0, 10).map((start2) => {
|
|
724
|
+
const ts = start2 + prefix.length;
|
|
725
|
+
return {
|
|
726
|
+
anchor_start: ts,
|
|
727
|
+
anchor_end: ts + text.length,
|
|
728
|
+
preview_before: body.slice(Math.max(0, ts - PREVIEW), ts),
|
|
729
|
+
preview_after: body.slice(ts + text.length, Math.min(body.length, ts + text.length + PREVIEW))
|
|
730
|
+
};
|
|
731
|
+
}),
|
|
732
|
+
hint: "prefix+text+suffix still has multiple matches; widen the surrounding context."
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
const start = indices[0] + prefix.length;
|
|
736
|
+
return success(body, text, start, version, "high");
|
|
737
|
+
}
|
|
738
|
+
function anchorInSection(body, version, heading, text) {
|
|
739
|
+
if (text.length === 0) {
|
|
740
|
+
return { error: "invalid_input", hint: "text must be non-empty" };
|
|
741
|
+
}
|
|
742
|
+
const sections = findSections(body, heading);
|
|
743
|
+
if (sections.length === 0) {
|
|
744
|
+
return { error: "no_match", hint: `heading "${heading}" not found in page body` };
|
|
745
|
+
}
|
|
746
|
+
if (sections.length > 1) {
|
|
747
|
+
return {
|
|
748
|
+
error: "ambiguous_anchor",
|
|
749
|
+
match_count: sections.length,
|
|
750
|
+
matches: sections.slice(0, 10).map((s) => ({
|
|
751
|
+
anchor_start: s.headingStart,
|
|
752
|
+
anchor_end: s.headingEnd,
|
|
753
|
+
preview_before: body.slice(Math.max(0, s.headingStart - PREVIEW), s.headingStart),
|
|
754
|
+
preview_after: body.slice(s.headingEnd, Math.min(body.length, s.headingEnd + PREVIEW))
|
|
755
|
+
})),
|
|
756
|
+
hint: `heading "${heading}" matches ${sections.length} sections; pass the exact heading title to disambiguate.`
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
const section = sections[0];
|
|
760
|
+
const sectionBody = body.slice(section.start, section.end);
|
|
761
|
+
const indicesInSection = findAll(sectionBody, text);
|
|
762
|
+
if (indicesInSection.length === 0) {
|
|
763
|
+
return { error: "no_match", hint: `text not found under heading "${heading}"` };
|
|
764
|
+
}
|
|
765
|
+
if (indicesInSection.length > 1) {
|
|
766
|
+
return {
|
|
767
|
+
error: "ambiguous_anchor",
|
|
768
|
+
match_count: indicesInSection.length,
|
|
769
|
+
matches: indicesInSection.slice(0, 10).map((local) => {
|
|
770
|
+
const start2 = section.start + local;
|
|
771
|
+
return {
|
|
772
|
+
anchor_start: start2,
|
|
773
|
+
anchor_end: start2 + text.length,
|
|
774
|
+
preview_before: body.slice(Math.max(0, start2 - PREVIEW), start2),
|
|
775
|
+
preview_after: body.slice(start2 + text.length, Math.min(body.length, start2 + text.length + PREVIEW))
|
|
776
|
+
};
|
|
777
|
+
}),
|
|
778
|
+
hint: `text appears multiple times under heading "${heading}"; use prefix+text+suffix instead.`
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
const start = section.start + indicesInSection[0];
|
|
782
|
+
return success(body, text, start, version, "high");
|
|
783
|
+
}
|
|
784
|
+
function findAll(body, needle) {
|
|
785
|
+
const out = [];
|
|
786
|
+
let i = body.indexOf(needle);
|
|
787
|
+
while (i !== -1) {
|
|
788
|
+
out.push(i);
|
|
789
|
+
i = body.indexOf(needle, i + 1);
|
|
790
|
+
}
|
|
791
|
+
return out;
|
|
792
|
+
}
|
|
793
|
+
function findSections(body, heading) {
|
|
794
|
+
const headingRe = /^(#{1,6})\s+(.+?)\s*$/gm;
|
|
795
|
+
const wanted = heading.replace(/^#+\s*/, "").trim();
|
|
796
|
+
const headings = [];
|
|
797
|
+
let match;
|
|
798
|
+
while ((match = headingRe.exec(body)) !== null) {
|
|
799
|
+
headings.push({
|
|
800
|
+
level: match[1].length,
|
|
801
|
+
title: match[2].trim(),
|
|
802
|
+
headingStart: match.index,
|
|
803
|
+
bodyStart: match.index + match[0].length
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
const exact = headings.filter((h) => h.title === wanted);
|
|
807
|
+
const candidates = exact.length > 0 ? exact : headings.filter((h) => h.title.includes(wanted));
|
|
808
|
+
return candidates.map((h) => {
|
|
809
|
+
const myIndex = headings.indexOf(h);
|
|
810
|
+
let end = body.length;
|
|
811
|
+
for (let j = myIndex + 1; j < headings.length; j++) {
|
|
812
|
+
if (headings[j].level <= h.level) {
|
|
813
|
+
end = headings[j].headingStart;
|
|
814
|
+
break;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return { start: h.bodyStart, end, headingStart: h.headingStart, headingEnd: h.bodyStart };
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
function success(body, text, start, version, confidence) {
|
|
821
|
+
const end = start + text.length;
|
|
822
|
+
return {
|
|
823
|
+
anchor_text: text,
|
|
824
|
+
anchor_start: start,
|
|
825
|
+
anchor_end: end,
|
|
826
|
+
anchor_version: version,
|
|
827
|
+
match_count: 1,
|
|
828
|
+
confidence,
|
|
829
|
+
preview_before: body.slice(Math.max(0, start - PREVIEW), start),
|
|
830
|
+
preview_after: body.slice(end, Math.min(body.length, end + PREVIEW))
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// src/tools/page-comments.ts
|
|
653
835
|
function registerPageCommentTools(server2, client2) {
|
|
836
|
+
server2.tool(
|
|
837
|
+
"cla_list_page_comments",
|
|
838
|
+
"List comments on a page. Returns root comments with their threaded replies. Use the resolved filter to focus on open threads (resolved=false) or only the resolved ones (resolved=true). Pass project_id to use the nested route; omit it and the server resolves the page via the api key\u2019s accessible_pages scope.",
|
|
839
|
+
{
|
|
840
|
+
page_id: z8.coerce.number().describe("Page ID to list comments for"),
|
|
841
|
+
project_id: z8.coerce.number().optional().describe("Project ID (optional \u2014 omit to use the flat route)"),
|
|
842
|
+
resolved: z8.boolean().optional().describe("Filter: true returns only resolved comments, false returns only unresolved")
|
|
843
|
+
},
|
|
844
|
+
async (params) => {
|
|
845
|
+
try {
|
|
846
|
+
const opts = params.resolved === void 0 ? void 0 : { resolved: params.resolved };
|
|
847
|
+
const result = params.project_id === void 0 ? await client2.listPageCommentsFlat(params.page_id, opts) : await client2.listPageComments(params.project_id, params.page_id, opts);
|
|
848
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
849
|
+
} catch (error) {
|
|
850
|
+
return handleError(error, client2.timeoutMs);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
);
|
|
854
|
+
server2.tool(
|
|
855
|
+
"cla_prepare_page_comment_anchor",
|
|
856
|
+
"Compute validated offsets for an anchored page comment without manually counting characters. Pass one locator: exact_text (must occur exactly once in the page body), or prefix+text+suffix to disambiguate repeated text via surrounding context, or heading+text to restrict the search to a markdown section. Returns anchor_text + anchor_start + anchor_end + anchor_version ready to feed into cla_create_page_comment. Returns a structured ambiguous_anchor error (with the list of matches and their preview context) when the locator still resolves to more than one position \u2014 never silently guesses.",
|
|
857
|
+
{
|
|
858
|
+
page_id: z8.coerce.number().describe("Page ID to anchor the comment on"),
|
|
859
|
+
project_id: z8.coerce.number().optional().describe("Project ID (optional \u2014 inferred via the flat page route)"),
|
|
860
|
+
exact_text: z8.string().optional().describe("Unique substring to anchor to (errors with ambiguous_anchor if not unique)"),
|
|
861
|
+
prefix: z8.string().optional().describe("Text immediately before the anchor (use with text + suffix to disambiguate)"),
|
|
862
|
+
text: z8.string().optional().describe("The selection to anchor (use with prefix+suffix, or with heading)"),
|
|
863
|
+
suffix: z8.string().optional().describe("Text immediately after the anchor (use with prefix + text)"),
|
|
864
|
+
heading: z8.string().optional().describe("Markdown heading whose section to search within (use with text)")
|
|
865
|
+
},
|
|
866
|
+
async (params) => {
|
|
867
|
+
try {
|
|
868
|
+
const pageResponse = params.project_id === void 0 ? await client2.getPageFlat(params.page_id) : await client2.getPage(params.project_id, params.page_id);
|
|
869
|
+
const page = pageResponse.page;
|
|
870
|
+
const body = page.body ?? "";
|
|
871
|
+
if (typeof page.version_number !== "number") {
|
|
872
|
+
const compat = {
|
|
873
|
+
error: "invalid_input",
|
|
874
|
+
hint: "page response is missing version_number \u2014 upgrade the Clarabit server (or pass anchor_text + offsets manually to cla_create_page_comment)"
|
|
875
|
+
};
|
|
876
|
+
return { content: [{ type: "text", text: JSON.stringify(compat, null, 2) }] };
|
|
877
|
+
}
|
|
878
|
+
const result = computeAnchor(body, page.version_number, {
|
|
879
|
+
exact_text: params.exact_text,
|
|
880
|
+
prefix: params.prefix,
|
|
881
|
+
text: params.text,
|
|
882
|
+
suffix: params.suffix,
|
|
883
|
+
heading: params.heading
|
|
884
|
+
});
|
|
885
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
886
|
+
} catch (error) {
|
|
887
|
+
return handleError(error, client2.timeoutMs);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
);
|
|
654
891
|
server2.tool(
|
|
655
892
|
"cla_create_page_comment",
|
|
656
|
-
"Post a comment on a page. Optionally thread under another comment via parent_comment_id, or anchor to a text selection via anchor_text/anchor_start/anchor_end.",
|
|
893
|
+
"Post a comment on a page. Optionally thread under another comment via parent_comment_id, or anchor to a text selection via anchor_text/anchor_start/anchor_end (+ optionally anchor_version to record the body revision the anchor was computed against). For anchored comments, prefer calling cla_prepare_page_comment_anchor first to get validated offsets \u2014 it handles ambiguous repeated text instead of silently anchoring to the first match.",
|
|
657
894
|
{
|
|
658
895
|
project_id: z8.coerce.number().optional().describe("Project ID (optional \u2014 inferred if omitted)"),
|
|
659
896
|
page_id: z8.coerce.number().describe("Page ID to comment on"),
|