Update all MCP tools to Markdown (#13234)

* Update all MCP tools to MD input+output

* feedback
This commit is contained in:
Tom Moor
2026-08-02 07:58:15 -04:00
committed by GitHub
parent 273a146381
commit b1b3db3430
7 changed files with 68 additions and 34 deletions
+14 -1
View File
@@ -20,7 +20,7 @@ import {
import type { ProsemirrorData, ReactionSummary } from "@shared/types";
import { ProsemirrorHelper } from "@shared/utils/ProsemirrorHelper";
import { CommentValidation } from "@shared/validations";
import { commentSchema } from "@server/editor";
import { commentSchema, serializer } from "@server/editor";
import { ValidationError } from "@server/errors";
import { CacheHelper } from "@server/utils/CacheHelper";
import { RedisPrefixHelper } from "@server/utils/RedisPrefixHelper";
@@ -148,6 +148,19 @@ class Comment extends ParanoidModel<
return ProsemirrorHelper.toPlainText(node);
}
/**
* Convert the comment data to markdown.
*
* @returns The markdown representation of the comment data
*/
public toMarkdown() {
const node = Node.fromJSON(commentSchema, this.data);
return serializer
.serialize(node)
.replace(/(^|\n)\\(\n|$)/g, "\n\n")
.trim();
}
// hooks
// A reply created on an already-resolved thread inherits the parent's
+21 -13
View File
@@ -11,6 +11,10 @@ type Options = {
shareId?: string;
/** Whether to include the updatedAt timestamp. */
includeUpdatedAt?: boolean;
/** Always include the markdown description in the payload. */
includeText?: boolean;
/** Always include the data of the collection in the payload. */
includeData?: boolean;
};
export default async function presentCollection(
@@ -25,19 +29,23 @@ export default async function presentCollection(
url: collection.path,
urlId: collection.urlId,
name: collection.name,
data: asData
? await DocumentHelper.toJSON(
collection,
options.isPublic
? {
signedUrls: Hour.seconds,
teamId: collection.teamId,
internalUrlBase: `/s/${options.shareId}`,
}
: undefined
)
: undefined,
description: asData ? undefined : collection.description,
data:
options.includeData === false
? undefined
: asData || options.includeData
? await DocumentHelper.toJSON(
collection,
options.isPublic
? {
signedUrls: Hour.seconds,
teamId: collection.teamId,
internalUrlBase: `/s/${options.shareId}`,
}
: undefined
)
: undefined,
description:
!asData || options.includeText ? collection.description : undefined,
sort: collection.sort,
icon: collection.icon,
color: collection.color,
+3 -1
View File
@@ -50,13 +50,15 @@ describe("collection tools", () => {
const res = await callMcpTool(server, accessToken, "create_collection", {
name: "Test Collection",
description: "A test description",
description: "A **test** description",
icon: "rocket",
color: "#FF0000",
});
const data = JSON.parse(res?.result?.content?.[0]?.text ?? "{}");
expect(data.name).toEqual("Test Collection");
expect(data.description).toEqual("A **test** description");
expect(data.data).toBeUndefined();
expect(data.icon).toEqual("rocket");
expect(data.color).toEqual("#FF0000");
expect(data.id).toBeDefined();
+19 -7
View File
@@ -5,7 +5,7 @@ import { Collection, Team } from "@server/models";
import { sequelize } from "@server/storage/database";
import { QueryHelper } from "@server/storage/QueryHelper";
import { authorize } from "@server/policies";
import { presentCollection } from "@server/presenters";
import { presentCollection as presentCollectionBase } from "@server/presenters";
import AuthenticationHelper from "@shared/helpers/AuthenticationHelper";
import { UrlHelper } from "@shared/utils/UrlHelper";
import {
@@ -20,6 +20,21 @@ import {
withTracing,
} from "./util";
/**
* Presents a collection for a tool response. Includes a markdown description
* instead of ProseMirror JSON so that MCP consumers (typically AI agents) can
* read it directly.
*
* @param collection - the collection to present.
* @returns the presented collection object.
*/
export function presentCollection(collection: Collection) {
return presentCollectionBase(undefined, collection, {
includeData: false,
includeText: true,
});
}
/**
* Registers collection-related MCP tools on the given server, filtered by
* the OAuth scopes granted to the current token.
@@ -125,7 +140,7 @@ export function collectionTools(server: McpServer, scopes: string[]) {
collection,
presented: pathToUrl(
user.team,
await presentCollection(undefined, collection)
await presentCollection(collection)
),
}))
),
@@ -201,7 +216,7 @@ export function collectionTools(server: McpServer, scopes: string[]) {
const presented = pathToUrl(
user.team,
await presentCollection(undefined, reloaded)
await presentCollection(reloaded)
);
return success(presented);
} catch (message) {
@@ -288,10 +303,7 @@ export function collectionTools(server: McpServer, scopes: string[]) {
collection.id
);
const presented = {
...pathToUrl(
user.team,
await presentCollection(undefined, collection)
),
...pathToUrl(user.team, await presentCollection(collection)),
...(shareUrl !== undefined && { shareUrl }),
};
return success(presented);
+3 -1
View File
@@ -132,12 +132,14 @@ describe("create_comment", () => {
const res = await callMcpTool(server, accessToken, "create_comment", {
documentId: document.id,
text: "This is a test comment",
text: "This is a **test** comment",
});
const data = JSON.parse(res?.result?.content?.[0]?.text ?? "{}");
expect(data.id).toBeDefined();
expect(data.documentId).toEqual(document.id);
expect(data.text).toEqual("This is a **test** comment");
expect(data.data).toBeUndefined();
});
it("creates a reply to an existing comment", async () => {
+5 -5
View File
@@ -25,25 +25,25 @@ import {
import { ValidationError } from "@server/errors";
/**
* Presents a comment with a plain-text rendering of its content so that
* Presents a comment with a markdown rendering of its content so that
* MCP consumers (typically AI agents) can read it without parsing
* ProseMirror JSON.
* ProseMirror JSON, which is omitted from the response.
*
* @param comment - the comment model instance.
* @param commentMarks - optional precomputed comment marks to avoid reparsing.
* @returns the presented comment with an additional `text` field.
* @returns the presented comment with a markdown `text` field.
*/
function presentCommentWithText(
comment: Comment,
commentMarks?: CommentMark[]
) {
const presented = presentComment(comment, {
const { data: _data, ...presented } = presentComment(comment, {
includeAnchorText: true,
commentMarks,
});
return {
...presented,
text: comment.toPlainText(),
text: comment.toMarkdown(),
};
}
+3 -6
View File
@@ -10,12 +10,9 @@ import {
} from "@server/models";
import { authorize, can } from "@server/policies";
import { AuthorizationError } from "@server/errors";
import {
presentCollection,
presentNavigationNode,
presentUser,
} from "@server/presenters";
import { presentNavigationNode, presentUser } from "@server/presenters";
import AuthenticationHelper from "@shared/helpers/AuthenticationHelper";
import { presentCollection } from "./collections";
import { presentDocument } from "./documents";
import { presentTemplate } from "./templates";
import {
@@ -172,7 +169,7 @@ export function fetchTool(server: McpServer, scopes: string[]) {
authorize(actor, "read", collection);
const [presented, shareUrl] = await Promise.all([
presentCollection(undefined, collection),
presentCollection(collection),
getPublicShareUrlForCollection(actor.team, collection.id),
]);
return success([