some tools
Some checks failed
CI / Test (pull_request) Failing after 56s

This commit is contained in:
2025-07-15 19:31:10 +02:00
parent fcaff53fb4
commit 912a0f7351
8 changed files with 148 additions and 90 deletions

8
src/config.ts Normal file
View File

@@ -0,0 +1,8 @@
import * as envVar from "env-var";
const config = {
baseUrl: envVar.get("OUTLINE_BASE_URL").required().asString(),
apiKey: envVar.get("OUTLINE_API_KEY").required().asString(),
};
export default config;

View File

@@ -1,17 +1,14 @@
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { server } from "./outline.js"; import pino from "pino";
import { outlineMcpFactory } from "./outline";
async function main() { async function main() {
// Add debugging const logger = pino();
console.error("Current working directory:", process.cwd());
console.error("__dirname:", __dirname);
const transport = new StdioServerTransport(); const transport = new StdioServerTransport();
await server.connect(transport); const server = outlineMcpFactory(logger, transport);
console.error("Outline MCP server is running via stdio transport");
server.start();
} }
main().catch((error) => { main();
console.error("Server error:", error);
process.exit(1);
});

View File

@@ -1,13 +1,38 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { registerDocumentTools } from "./tools/document";
import config from "./config";
import { CollectionsApi, Configuration, DocumentsApi } from "./gen/api/outline";
import { registerCollectionTools } from "./tools/collection"; import { registerCollectionTools } from "./tools/collection";
import { registerDocumentTools } from "./tools/document";
// Create an MCP server import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
export const server = new McpServer({ import type { Logger } from "pino";
name: "outline-mcp-server",
const createOutlineClient = (baseUrl: string, apiKey: string) => {
const config = new Configuration({
basePath: baseUrl,
accessToken: () => Promise.resolve(apiKey),
});
return {
documents: new DocumentsApi(config),
collections: new CollectionsApi(config),
};
};
export const outlineMcpFactory = (logger: Logger, transport: Transport) => {
const server = new McpServer({
name: "outline-mcp",
version: "1.0.0", version: "1.0.0",
}); });
// Register tools const client = createOutlineClient(config.baseUrl, config.apiKey);
registerDocumentTools(server); registerDocumentTools(server, client.documents, logger);
registerCollectionTools(server); registerCollectionTools(server, client.collections, logger);
const start = () => server.connect(transport);
return {
start,
};
};

View File

@@ -1,10 +1,14 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { createOutlineClient, handleSuccess, handleError } from "./utils";
// Register collection tools import { handleSuccess, handleError } from "./utils";
export const registerCollectionTools = (server: McpServer) => {
import type { ToolsFactory } from "./toolsFactory";
import type { CollectionsApi } from "../gen/api/outline";
import type { Logger } from "pino";
export const registerCollectionTools: ToolsFactory<CollectionsApi> = (server: McpServer, client: CollectionsApi, logger: Logger) => {
server.registerTool( server.registerTool(
"collections_list", "collections.list",
{ {
title: "List Collections", title: "List Collections",
description: "List all collections in the Outline workspace", description: "List all collections in the Outline workspace",
@@ -12,11 +16,12 @@ export const registerCollectionTools = (server: McpServer) => {
}, },
async () => { async () => {
try { try {
const client = createOutlineClient(); const response = await client.collectionsList();
const response = await client.collections.collectionsList(); return handleSuccess(response, logger.child({ tool: "collections.list" }));
return handleSuccess(response); } catch (err) {
} catch (error) { const error = handleError(err, logger.child({ tool: "collections.list" }));
return handleError(error); logger.error(error);
return error;
} }
} }
); );

19
src/tools/common.ts Normal file
View File

@@ -0,0 +1,19 @@
import { z } from "zod";
const positiveInteger = z.number().int().positive();
const uuid = z.string().uuid();
const optionalUuid = uuid.optional();
// Pagination
export const offset = positiveInteger.optional();
export const limit = positiveInteger.optional().default(10);
export const sort = z.enum(["createdAt", "updatedAt", "title"]).optional();
export const direction = z
.enum(["asc", "desc"])
.optional()
.transform((val) => val?.toUpperCase() as "ASC" | "DESC" | undefined);
export const collectionId = optionalUuid;
export const userId = optionalUuid;
export const backlinkDocumentId = optionalUuid;
export const parentDocumentId = optionalUuid;

View File

@@ -1,49 +1,67 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod"; import { z } from "zod";
import { createOutlineClient, handleSuccess, handleError } from "./utils";
// Register document tools import { backlinkDocumentId, collectionId, direction, limit, offset, parentDocumentId, sort, userId } from "./common";
export const registerDocumentTools = (server: McpServer) => { import { handleSuccess, handleError } from "./utils";
server.registerTool(
"documents_list",
{
title: "List Documents",
description: "List all documents in the Outline workspace",
inputSchema: {},
},
async () => {
try {
const client = createOutlineClient();
const response = await client.documents.documentsList();
return handleSuccess(response);
} catch (error) {
return handleError(error);
}
}
);
import type { ToolsFactory } from "./toolsFactory";
import type { DocumentsApi } from "../gen/api/outline";
import type { Logger } from "pino";
export const registerDocumentTools: ToolsFactory<DocumentsApi> = (server: McpServer, client: DocumentsApi, logger: Logger): void => {
server.registerTool( server.registerTool(
"documents_info", "documents.info",
{ {
title: "Get Document Info", title: "Get Document Info",
description: "Retrieve a specific document by its ID or share ID", description: "Retrieve a document by its UUID, urlId, or shareId. At least one of these parameters must be provided.",
inputSchema: { inputSchema: {
id: z.string().optional().describe("Document UUID or URL ID"), id: z.string().optional().describe("Unique identifier for the document. Either the UUID or the urlId is acceptable."),
shareId: z.string().optional().describe("Document share ID"), shareId: z.string().optional().describe("Unique identifier for a document share, a shareId may be used in place of a document UUID"),
}, },
}, },
async (args) => { async (args) => {
try { try {
const client = createOutlineClient(); const response = await client.documentsInfo({
const response = await client.documents.documentsInfo({
documentsInfoRequest: { documentsInfoRequest: {
id: args?.id, id: args?.id,
shareId: args?.shareId, shareId: args?.shareId,
}, },
}); });
return handleSuccess(response); return handleSuccess(response, logger.child({ tool: "documents.info" }));
} catch (error) { } catch (error) {
return handleError(error); return handleError(error, logger.child({ tool: "documents.info" }));
}
}
);
const documentsListSchema = z.object({
offset,
limit,
sort,
direction,
collectionId,
userId,
backlinkDocumentId,
parentDocumentId,
template: z.boolean().optional(),
});
server.registerTool(
"documents.list",
{
title: "List Documents",
description: "This method will list all published documents and draft documents belonging to the current user.",
inputSchema: documentsListSchema.shape,
},
async (args) => {
try {
const validatedArgs = documentsListSchema.parse(args);
const response = await client.documentsList({
documentsListRequest: validatedArgs,
});
return handleSuccess(response, logger.child({ tool: "documents.list" }));
} catch (error) {
return handleError(error, logger.child({ tool: "documents.list" }));
} }
} }
); );

View File

@@ -0,0 +1,5 @@
import type { BaseAPI } from "../gen/api/outline";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { Logger } from "pino";
export type ToolsFactory<T extends BaseAPI> = (server: McpServer, client: T, logger: Logger) => void;

View File

@@ -1,47 +1,28 @@
import { Configuration } from "../gen/api/outline/runtime"; import type { Logger } from "pino";
import { DocumentsApi } from "../gen/api/outline/apis/DocumentsApi";
import { CollectionsApi } from "../gen/api/outline/apis/CollectionsApi";
// Generic Outline API client factory export const handleError = (error: unknown, logger: Logger) => {
export const createOutlineClient = () => { const message = `Error: ${error instanceof Error ? error.message : "Unknown error"}`;
const apiKey = process.env.OUTLINE_API_KEY;
if (!apiKey) {
throw new Error("OUTLINE_API_KEY environment variable is not set");
}
const config = new Configuration({ logger.error(message);
basePath: process.env.OUTLINE_BASE_URL,
accessToken: () => Promise.resolve(apiKey),
});
return {
documents: new DocumentsApi(config),
collections: new CollectionsApi(config),
};
};
// Generic response handler
export const handleApiResponse = (response: any) => {
return JSON.stringify(response, null, 2);
};
// Generic error response handler
export const handleError = (error: unknown) => {
return { return {
content: [ content: [
{ {
type: "text" as const, type: "text" as const,
text: `Error: ${ text: message,
error instanceof Error ? error.message : "Unknown error"
}`,
}, },
], ],
}; };
}; };
// Generic success response handler const handleApiResponse = (response: unknown) => {
export const handleSuccess = (data: any) => { return JSON.stringify(response, null, 2);
};
export const handleSuccess = (data: unknown, logger: Logger) => {
const message = handleApiResponse(data);
logger.debug(message);
return { return {
content: [{ type: "text" as const, text: handleApiResponse(data) }], content: [{ type: "text" as const, text: message }],
}; };
}; };