mirror of
https://github.com/outline/outline.git
synced 2026-08-03 13:27:25 +03:00
Update all MCP tools to Markdown (#13234)
* Update all MCP tools to MD input+output * feedback
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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([
|
||||
|
||||
Reference in New Issue
Block a user