feat: add outline MCP server
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m56s
Monorepo Pipeline / 🧹 Lint (push) Failing after 3m24s
Monorepo Pipeline / 🏗️ Build (push) Successful in 4m11s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧪 Test (push) Successful in 1m56s
Monorepo Pipeline / 🧹 Lint (push) Failing after 3m24s
Monorepo Pipeline / 🏗️ Build (push) Successful in 4m11s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
This commit is contained in:
@@ -97,6 +97,18 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- mcp-network
|
- mcp-network
|
||||||
|
|
||||||
|
outline-mcp:
|
||||||
|
build:
|
||||||
|
context: ./packages/outline-mcp
|
||||||
|
container_name: outline-mcp
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
ports:
|
||||||
|
- "3008:3008"
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- mcp-network
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
mcp-network:
|
mcp-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|||||||
15
packages/outline-mcp/Dockerfile
Normal file
15
packages/outline-mcp/Dockerfile
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
FROM node:20-bookworm-slim AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json ./
|
||||||
|
RUN npm install -g pnpm@10.18.3 && pnpm install --ignore-workspace
|
||||||
|
COPY tsconfig.json ./
|
||||||
|
COPY src ./src
|
||||||
|
RUN pnpm build
|
||||||
|
|
||||||
|
FROM node:20-bookworm-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /app/package.json ./
|
||||||
|
COPY --from=builder /app/node_modules ./node_modules
|
||||||
|
COPY --from=builder /app/dist ./dist
|
||||||
|
|
||||||
|
ENTRYPOINT ["node", "dist/start.js"]
|
||||||
26
packages/outline-mcp/package.json
Normal file
26
packages/outline-mcp/package.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "@mintel/outline-mcp",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Outline MCP server for Mintel infrastructure",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/start.js",
|
||||||
|
"dev": "tsx watch src/index.ts",
|
||||||
|
"test:unit": "vitest run"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.5.0",
|
||||||
|
"axios": "^1.7.2",
|
||||||
|
"dotenv": "^17.3.1",
|
||||||
|
"express": "^4.19.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/node": "^20.14.10",
|
||||||
|
"tsx": "^4.19.2",
|
||||||
|
"typescript": "^5.5.3",
|
||||||
|
"vitest": "^2.1.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
91
packages/outline-mcp/src/index.test.ts
Normal file
91
packages/outline-mcp/src/index.test.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||||
|
|
||||||
|
// Mock environment
|
||||||
|
process.env.OUTLINE_API_TOKEN = 'test-token';
|
||||||
|
process.env.OUTLINE_BASE_URL = 'https://outline.test';
|
||||||
|
|
||||||
|
// Mock axios
|
||||||
|
vi.mock('axios', () => {
|
||||||
|
const mockPost = vi.fn();
|
||||||
|
return {
|
||||||
|
default: {
|
||||||
|
create: vi.fn(() => ({
|
||||||
|
post: mockPost
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Outline MCP Server Tools', () => {
|
||||||
|
let mockPost: any;
|
||||||
|
let requestHandler: any;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
|
||||||
|
// Extract the mock
|
||||||
|
const axiosInstance = axios.create();
|
||||||
|
mockPost = axiosInstance.post;
|
||||||
|
|
||||||
|
// Mock Server setRequestHandler
|
||||||
|
let capturedHandler: any;
|
||||||
|
vi.spyOn(Server.prototype, 'setRequestHandler').mockImplementation((schema: any, handler: any) => {
|
||||||
|
// The first call is usually ListToolsRequestSchema, second is CallToolRequestSchema
|
||||||
|
// We just store the last one which is CallToolRequestSchema
|
||||||
|
capturedHandler = handler;
|
||||||
|
return {} as any;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-import to trigger registration
|
||||||
|
vi.resetModules();
|
||||||
|
await import('./index.js');
|
||||||
|
|
||||||
|
requestHandler = capturedHandler;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should search documents', async () => {
|
||||||
|
mockPost.mockResolvedValueOnce({ data: { data: [{ id: '1', title: 'Test' }] } });
|
||||||
|
|
||||||
|
const result = await requestHandler({
|
||||||
|
params: {
|
||||||
|
name: 'outline_search_documents',
|
||||||
|
arguments: { query: 'test' }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockPost).toHaveBeenCalledWith('/api/documents.search', { query: 'test', collectionId: undefined });
|
||||||
|
expect(result.content[0].text).toContain('Test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get a document', async () => {
|
||||||
|
mockPost.mockResolvedValueOnce({ data: { data: { id: 'doc1', title: 'Doc 1' } } });
|
||||||
|
|
||||||
|
const result = await requestHandler({
|
||||||
|
params: {
|
||||||
|
name: 'outline_get_document',
|
||||||
|
arguments: { id: 'doc1' }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockPost).toHaveBeenCalledWith('/api/documents.info', { id: 'doc1' });
|
||||||
|
expect(result.content[0].text).toContain('Doc 1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a document', async () => {
|
||||||
|
mockPost.mockResolvedValueOnce({ data: { data: { id: 'new-doc' } } });
|
||||||
|
|
||||||
|
const result = await requestHandler({
|
||||||
|
params: {
|
||||||
|
name: 'outline_create_document',
|
||||||
|
arguments: { collectionId: 'col1', title: 'New Doc', text: 'Hello', publish: true }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockPost).toHaveBeenCalledWith('/api/documents.create', {
|
||||||
|
collectionId: 'col1', title: 'New Doc', text: 'Hello', publish: true
|
||||||
|
});
|
||||||
|
expect(result.content[0].text).toContain('new-doc');
|
||||||
|
});
|
||||||
|
});
|
||||||
184
packages/outline-mcp/src/index.ts
Normal file
184
packages/outline-mcp/src/index.ts
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
|
import {
|
||||||
|
CallToolRequestSchema,
|
||||||
|
ListToolsRequestSchema,
|
||||||
|
Tool,
|
||||||
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
// Environment variables
|
||||||
|
const OUTLINE_API_TOKEN = process.env.OUTLINE_API_TOKEN;
|
||||||
|
const OUTLINE_BASE_URL = process.env.OUTLINE_BASE_URL;
|
||||||
|
|
||||||
|
if (!OUTLINE_API_TOKEN || !OUTLINE_BASE_URL) {
|
||||||
|
console.error("Missing required environment variables: OUTLINE_API_TOKEN or OUTLINE_BASE_URL");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const axiosInstance = axios.create({
|
||||||
|
baseURL: OUTLINE_BASE_URL,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${OUTLINE_API_TOKEN}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Define tools
|
||||||
|
const tools: Tool[] = [
|
||||||
|
{
|
||||||
|
name: "outline_search_documents",
|
||||||
|
description: "Search for documents in Outline.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
query: { type: "string", description: "Search query" },
|
||||||
|
collectionId: { type: "string", description: "Optional collection ID to scope search" },
|
||||||
|
},
|
||||||
|
required: ["query"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "outline_get_document",
|
||||||
|
description: "Get a document by its ID.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
id: { type: "string", description: "The ID of the document" },
|
||||||
|
},
|
||||||
|
required: ["id"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "outline_create_document",
|
||||||
|
description: "Create a new document.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
collectionId: { type: "string", description: "The ID of the collection" },
|
||||||
|
title: { type: "string", description: "Document title" },
|
||||||
|
text: { type: "string", description: "Document content in Markdown" },
|
||||||
|
publish: { type: "boolean", description: "Whether to publish the document" },
|
||||||
|
},
|
||||||
|
required: ["collectionId", "title", "text"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "outline_update_document",
|
||||||
|
description: "Update an existing document.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
id: { type: "string", description: "The ID of the document" },
|
||||||
|
title: { type: "string", description: "New title" },
|
||||||
|
text: { type: "string", description: "New content in Markdown" },
|
||||||
|
},
|
||||||
|
required: ["id"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "outline_create_collection",
|
||||||
|
description: "Create a new collection.",
|
||||||
|
inputSchema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
name: { type: "string", description: "Collection name" },
|
||||||
|
description: { type: "string", description: "Collection description" },
|
||||||
|
},
|
||||||
|
required: ["name"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const server = new Server(
|
||||||
|
{
|
||||||
|
name: "at-mintel-outline-mcp",
|
||||||
|
version: "1.0.0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
tools: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||||
|
return { tools };
|
||||||
|
});
|
||||||
|
|
||||||
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
|
try {
|
||||||
|
switch (request.params.name) {
|
||||||
|
case "outline_search_documents": {
|
||||||
|
const { query, collectionId } = request.params.arguments as any;
|
||||||
|
const res = await axiosInstance.post("/api/documents.search", {
|
||||||
|
query,
|
||||||
|
collectionId,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "outline_get_document": {
|
||||||
|
const { id } = request.params.arguments as any;
|
||||||
|
const res = await axiosInstance.post("/api/documents.info", { id });
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "outline_create_document": {
|
||||||
|
const { collectionId, title, text, publish } = request.params.arguments as any;
|
||||||
|
const res = await axiosInstance.post("/api/documents.create", {
|
||||||
|
collectionId,
|
||||||
|
title,
|
||||||
|
text,
|
||||||
|
publish: publish ?? true,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "outline_update_document": {
|
||||||
|
const { id, title, text } = request.params.arguments as any;
|
||||||
|
const res = await axiosInstance.post("/api/documents.update", {
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "outline_create_collection": {
|
||||||
|
const { name, description } = request.params.arguments as any;
|
||||||
|
const res = await axiosInstance.post("/api/collections.create", {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown tool: ${request.params.name}`);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
const errorMsg = error.response?.data ? JSON.stringify(error.response.data) : error.message;
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `API Error: ${errorMsg}` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await server.connect(transport);
|
||||||
|
console.error("Outline MCP Server running on stdio");
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error("Fatal error:", error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
13
packages/outline-mcp/src/start.ts
Normal file
13
packages/outline-mcp/src/start.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { config } from 'dotenv';
|
||||||
|
import { resolve } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
||||||
|
|
||||||
|
config({ quiet: true, path: resolve(__dirname, '../../../.env.local') });
|
||||||
|
config({ quiet: true, path: resolve(__dirname, '../../../.env') });
|
||||||
|
|
||||||
|
import('./index.js').catch(err => {
|
||||||
|
console.error('Failed to start Outline MCP Server:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
16
packages/outline-mcp/tsconfig.json
Normal file
16
packages/outline-mcp/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
31
pnpm-lock.yaml
generated
31
pnpm-lock.yaml
generated
@@ -754,6 +754,37 @@ importers:
|
|||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.1.9(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
|
version: 2.1.9(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
|
||||||
|
|
||||||
|
packages/outline-mcp:
|
||||||
|
dependencies:
|
||||||
|
'@modelcontextprotocol/sdk':
|
||||||
|
specifier: ^1.5.0
|
||||||
|
version: 1.27.1(zod@3.25.76)
|
||||||
|
axios:
|
||||||
|
specifier: ^1.7.2
|
||||||
|
version: 1.13.5
|
||||||
|
dotenv:
|
||||||
|
specifier: ^17.3.1
|
||||||
|
version: 17.3.1
|
||||||
|
express:
|
||||||
|
specifier: ^4.19.2
|
||||||
|
version: 4.22.1
|
||||||
|
devDependencies:
|
||||||
|
'@types/express':
|
||||||
|
specifier: ^4.17.21
|
||||||
|
version: 4.17.25
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^20.14.10
|
||||||
|
version: 20.19.33
|
||||||
|
tsx:
|
||||||
|
specifier: ^4.19.2
|
||||||
|
version: 4.21.0
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.5.3
|
||||||
|
version: 5.9.3
|
||||||
|
vitest:
|
||||||
|
specifier: ^2.1.3
|
||||||
|
version: 2.1.9(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
|
||||||
|
|
||||||
packages/page-audit:
|
packages/page-audit:
|
||||||
dependencies:
|
dependencies:
|
||||||
commander:
|
commander:
|
||||||
|
|||||||
Reference in New Issue
Block a user