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

View File

@@ -1,13 +1,38 @@
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 { registerDocumentTools } from "./tools/document";
// Create an MCP server
export const server = new McpServer({
name: "outline-mcp-server",
version: "1.0.0",
});
import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import type { Logger } from "pino";
// Register tools
registerDocumentTools(server);
registerCollectionTools(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",
});
const client = createOutlineClient(config.baseUrl, config.apiKey);
registerDocumentTools(server, client.documents, logger);
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 { createOutlineClient, handleSuccess, handleError } from "./utils";
// Register collection tools
export const registerCollectionTools = (server: McpServer) => {
import { handleSuccess, handleError } from "./utils";
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(
"collections_list",
"collections.list",
{
title: "List Collections",
description: "List all collections in the Outline workspace",
@@ -12,11 +16,12 @@ export const registerCollectionTools = (server: McpServer) => {
},
async () => {
try {
const client = createOutlineClient();
const response = await client.collections.collectionsList();
return handleSuccess(response);
} catch (error) {
return handleError(error);
const response = await client.collectionsList();
return handleSuccess(response, logger.child({ tool: "collections.list" }));
} catch (err) {
const error = handleError(err, logger.child({ tool: "collections.list" }));
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 { z } from "zod";
import { createOutlineClient, handleSuccess, handleError } from "./utils";
// Register document tools
export const registerDocumentTools = (server: McpServer) => {
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 { backlinkDocumentId, collectionId, direction, limit, offset, parentDocumentId, sort, userId } from "./common";
import { handleSuccess, handleError } from "./utils";
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(
"documents_info",
"documents.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: {
id: z.string().optional().describe("Document UUID or URL ID"),
shareId: z.string().optional().describe("Document share ID"),
id: z.string().optional().describe("Unique identifier for the document. Either the UUID or the urlId is acceptable."),
shareId: z.string().optional().describe("Unique identifier for a document share, a shareId may be used in place of a document UUID"),
},
},
async (args) => {
try {
const client = createOutlineClient();
const response = await client.documents.documentsInfo({
const response = await client.documentsInfo({
documentsInfoRequest: {
id: args?.id,
shareId: args?.shareId,
},
});
return handleSuccess(response);
return handleSuccess(response, logger.child({ tool: "documents.info" }));
} 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 { DocumentsApi } from "../gen/api/outline/apis/DocumentsApi";
import { CollectionsApi } from "../gen/api/outline/apis/CollectionsApi";
import type { Logger } from "pino";
// Generic Outline API client factory
export const createOutlineClient = () => {
const apiKey = process.env.OUTLINE_API_KEY;
if (!apiKey) {
throw new Error("OUTLINE_API_KEY environment variable is not set");
}
export const handleError = (error: unknown, logger: Logger) => {
const message = `Error: ${error instanceof Error ? error.message : "Unknown error"}`;
const config = new Configuration({
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) => {
logger.error(message);
return {
content: [
{
type: "text" as const,
text: `Error: ${
error instanceof Error ? error.message : "Unknown error"
}`,
text: message,
},
],
};
};
// Generic success response handler
export const handleSuccess = (data: any) => {
const handleApiResponse = (response: unknown) => {
return JSON.stringify(response, null, 2);
};
export const handleSuccess = (data: unknown, logger: Logger) => {
const message = handleApiResponse(data);
logger.debug(message);
return {
content: [{ type: "text" as const, text: handleApiResponse(data) }],
content: [{ type: "text" as const, text: message }],
};
};