@runbook-docs/mcp-server
Version:
Runbook Model Context Protocol Server
228 lines (217 loc) • 5.46 kB
text/typescript
import {
McpServer,
ResourceTemplate
} from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import Runbook from '@runbook-docs/client';
import {
GetArticleQuery,
GetArticlesQuery,
GetBooksQuery,
GetCategoriesQuery,
searchQuery
} from '@runbook-docs/client/dist/queries/types';
import config from './config';
const server = new McpServer({
name: 'Runbook',
version: '1.0.0'
});
const runbook = new Runbook(config);
server.resource(
'article',
new ResourceTemplate('runbook://articles/{articleUid}', { list: undefined }),
{
description: 'An article data',
mimeType: 'application/json'
},
async (uri, { articleUid }) => {
const data: GetArticleQuery = await runbook.query('getArticle', {
articleUid: articleUid.toString()
});
return {
contents: [
{
uri: uri.href,
text: JSON.stringify(
{
...data.node,
url: `${config.baseUrl}/articles/${articleUid}`
},
null,
2
),
mimeType: 'application/json'
}
]
};
}
);
server.tool(
'get-article',
'Retrieve the article by its ID from the database.',
{
articleUid: z
.string()
.describe('ID of the article to retrieve. It always starts with `ar_`.')
},
async ({ articleUid }) => {
const data: GetArticleQuery = await runbook.query('getArticle', {
articleUid
});
return {
content: [
{
type: 'text',
text: JSON.stringify(
{
...data.node,
url: `${config.baseUrl}/articles/${articleUid}`
},
null,
2
)
}
]
};
}
);
server.tool(
'list-articles',
`List top 100 articles in a specified book with ID.
The result does not include entire article bodies as they are truncated in 200 characters.
You have to retrieve the full content by calling \`get-article\`.
`,
{
bookUid: z
.string()
.describe(
`ID of the book. It always starts with 'bk_'. You can retrieve a list of books with \`list-books\``
),
q: z
.string()
.optional()
.describe(
`Search query. If provided, the result will be filtered by article name.`
),
categoryUid: z
.string()
.optional()
.describe(
`ID of the category. It always starts with 'ca_'. You can retrieve a list of categories with \`list-categories\``
),
orderBy: z
.enum(['updatedAt', 'createdAt', 'name', 'popularity'])
.optional()
.describe(`Sort the articles by the specified field`)
},
async (props) => {
const data: GetArticlesQuery = await runbook.query('getArticles', {
...props,
first: 100
});
return {
content: [
{
type: 'text',
text: JSON.stringify(data.node.articles.nodes, null, 2)
}
]
};
}
);
server.tool(
'list-books',
`List top 100 books in the organization`,
{
q: z
.string()
.describe(
`Search query. If provided, the result will be filtered by book name.`
)
},
async ({ q }) => {
const data: GetBooksQuery = await runbook.query('getBooks', {
q: q,
first: 100
});
return {
content: [
{
type: 'text',
text: JSON.stringify(data.organization.books.nodes, null, 2)
}
]
};
}
);
server.tool(
'list-categories',
`List top 100 categories in a specified book with ID`,
{
bookUid: z
.string()
.describe(
`ID of the book. It always starts with 'bk_'. You can retrieve a list of books with \`list-books\``
)
},
async ({ bookUid }) => {
const data: GetCategoriesQuery = await runbook.query('getCategories', {
bookUid,
first: 100
});
const categories = data.node.categories.nodes;
return {
content: [
{
type: 'text',
text: JSON.stringify(categories, null, 2)
}
]
};
}
);
server.tool(
'search-articles',
`Search articles by a query string.
`,
{
scope: z
.string()
.optional()
.default('all')
.describe(
`ID of the book or workspace. You can retrieve a list of books with \`list-books\``
),
keywords: z
.string()
.describe(
`Space-separated keywords to filter articles. If multiple keywords are provided, the articles that match all of them will be returned.`
),
limit: z.number().optional().describe(`Number of articles to retrieve`),
offset: z.number().optional().describe(`Offset of the search result`),
orderBy: z
.enum(['updatedAt', 'createdAt', 'score'])
.optional()
.default('score')
.describe(`Sort the articles by the specified field`)
},
async (params) => {
const data: searchQuery = await runbook.query('search', params);
const searchResults = data.searchResults.nodes;
return {
content: [
{
type: 'text',
text: JSON.stringify(searchResults, null, 2)
}
]
};
}
);
export async function runServer() {
console.error(`Base URL ${config.baseUrl}. Starting server.`);
const transport = new StdioServerTransport();
await server.connect(transport);
}