This commit is contained in:
8
src/config.ts
Normal file
8
src/config.ts
Normal 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;
|
||||||
19
src/main.ts
19
src/main.ts
@@ -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);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -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",
|
|
||||||
version: "1.0.0",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Register tools
|
const createOutlineClient = (baseUrl: string, apiKey: string) => {
|
||||||
registerDocumentTools(server);
|
const config = new Configuration({
|
||||||
registerCollectionTools(server);
|
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,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -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
19
src/tools/common.ts
Normal 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;
|
||||||
@@ -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" }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
5
src/tools/toolsFactory.ts
Normal file
5
src/tools/toolsFactory.ts
Normal 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;
|
||||||
@@ -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 }],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user