-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Expand file tree
/
Copy pathserver.ts
More file actions
105 lines (95 loc) · 3.64 KB
/
server.ts
File metadata and controls
105 lines (95 loc) · 3.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
import {buildPageIndex, ensureParsedPage, resolvePageRef} from './page-manager.js';
import {errorToString, fetchText} from './utils.js';
import type {Library} from './types.js';
import {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js';
import {parseSectionsFromMarkdown} from './parser.js';
import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
import {z} from 'zod';
export async function startServer(
library: Library,
version: string,
registerAdditionalTools?: (server: McpServer) => void | Promise<void>
) {
const server = new McpServer({
name: library === 's2' ? 's2-docs-server' : 'react-aria-docs-server',
version
});
// Build page index at startup.
try {
await buildPageIndex(library);
} catch (e) {
console.warn(`Warning: failed to load ${library} docs index (${errorToString(e)}).`);
}
const toolPrefix = library === 's2' ? 's2' : 'react_aria';
server.registerTool(
`list_${toolPrefix}_pages`,
{
title: library === 's2' ? 'List React Spectrum (@react-spectrum/s2) docs pages' : 'List React Aria docs pages',
description: `Returns a list of available pages in the ${library} docs.`,
inputSchema: {includeDescription: z.boolean().optional()},
annotations: {readOnlyHint: true}
},
async ({includeDescription}) => {
const pages = await buildPageIndex(library);
const items = pages
.sort((a, b) => a.key.localeCompare(b.key))
.map(p => includeDescription ? {name: p.name, description: p.description ?? ''} : {name: p.name});
return {
content: [{type: 'text', text: JSON.stringify(items, null, 2)}]
};
}
);
server.registerTool(
`get_${toolPrefix}_page_info`,
{
title: 'Get page info',
description: 'Returns page description and list of sections for a given page.',
inputSchema: {page_name: z.string()},
annotations: {readOnlyHint: true}
},
async ({page_name}) => {
const ref = await resolvePageRef(library, page_name);
const info = await ensureParsedPage(ref);
const out = {
name: info.name,
description: info.description ?? '',
sections: info.sections.map(s => s.name)
};
return {content: [{type: 'text', text: JSON.stringify(out, null, 2)}]};
}
);
server.registerTool(
`get_${toolPrefix}_page`,
{
title: 'Get page markdown',
description: 'Returns the full markdown content for a page, or a specific section if provided.',
inputSchema: {page_name: z.string(), section_name: z.string().optional()},
annotations: {readOnlyHint: true}
},
async ({page_name, section_name}) => {
const ref = await resolvePageRef(library, page_name);
let text: string;
text = await fetchText(ref.filePath);
if (!section_name) {
return {content: [{type: 'text', text}]} as const;
}
const lines = text.split(/\r?\n/);
const sections = parseSectionsFromMarkdown(lines);
let section = sections.find(s => s.name === section_name);
if (!section) {
section = sections.find(s => s.name.toLowerCase() === section_name.toLowerCase());
}
if (!section) {
const available = sections.map(s => s.name).join(', ');
throw new Error(`Section '${section_name}' not found in ${ref.key}. Available: ${available}`);
}
const snippet = lines.slice(section.startLine, section.endLine).join('\n');
return {content: [{type: 'text', text: snippet}]} as const;
}
);
if (registerAdditionalTools) {
await registerAdditionalTools(server);
}
const transport = new StdioServerTransport();
await server.connect(transport);
}