@arabold/docs-mcp-server 1.10.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +152 -232
- package/db/migrations/000-initial-schema.sql +57 -0
- package/db/migrations/001-add-indexed-at-column.sql +6 -0
- package/dist/DocumentManagementService-_qCZ1Hi2.js +3409 -0
- package/dist/DocumentManagementService-_qCZ1Hi2.js.map +1 -0
- package/dist/EmbeddingFactory-BJMbJvje.js +174 -0
- package/dist/EmbeddingFactory-BJMbJvje.js.map +1 -0
- package/dist/FindVersionTool-CH1c3Tyu.js +170 -0
- package/dist/FindVersionTool-CH1c3Tyu.js.map +1 -0
- package/dist/RemoveTool-DmB1YJTA.js +65 -0
- package/dist/RemoveTool-DmB1YJTA.js.map +1 -0
- package/dist/assets/main.css +1 -0
- package/dist/assets/main.js +8097 -0
- package/dist/assets/main.js.map +1 -0
- package/dist/cli.js +49 -143
- package/dist/cli.js.map +1 -1
- package/dist/server.js +684 -388
- package/dist/server.js.map +1 -1
- package/dist/web.js +937 -0
- package/dist/web.js.map +1 -0
- package/package.json +35 -11
- package/public/assets/main.css +1 -0
- package/public/assets/main.js +8097 -0
- package/public/assets/main.js.map +1 -0
- package/dist/EmbeddingFactory-6UEXNF44.js +0 -1177
- package/dist/EmbeddingFactory-6UEXNF44.js.map +0 -1
- package/dist/chunk-VTO2ED43.js +0 -12098
- package/dist/chunk-VTO2ED43.js.map +0 -1
- package/dist/chunk-YCXNASA6.js +0 -124
- package/dist/chunk-YCXNASA6.js.map +0 -1
- package/dist/cli.d.ts +0 -1
- package/dist/server.d.ts +0 -1
package/dist/server.js
CHANGED
|
@@ -1,34 +1,153 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
CancelJobTool,
|
|
4
|
-
DEFAULT_MAX_DEPTH,
|
|
5
|
-
DEFAULT_MAX_PAGES,
|
|
6
|
-
DocumentManagementService,
|
|
7
|
-
FetchUrlTool,
|
|
8
|
-
FileFetcher,
|
|
9
|
-
FindVersionTool,
|
|
10
|
-
GetJobInfoTool,
|
|
11
|
-
HttpFetcher,
|
|
12
|
-
ListJobsTool,
|
|
13
|
-
ListLibrariesTool,
|
|
14
|
-
PipelineJobStatus,
|
|
15
|
-
PipelineManager,
|
|
16
|
-
RemoveTool,
|
|
17
|
-
ScrapeTool,
|
|
18
|
-
SearchTool,
|
|
19
|
-
VersionNotFoundError,
|
|
20
|
-
logger,
|
|
21
|
-
setLogLevel
|
|
22
|
-
} from "./chunk-VTO2ED43.js";
|
|
23
|
-
import "./chunk-YCXNASA6.js";
|
|
24
|
-
|
|
25
|
-
// src/mcp/index.ts
|
|
26
2
|
import "dotenv/config";
|
|
3
|
+
import { program } from "commander";
|
|
4
|
+
import { l as logger, P as PipelineJobStatus, D as DocumentManagementService, a as PipelineManager, b as DEFAULT_MAX_DEPTH, c as DEFAULT_MAX_PAGES, L as LibraryNotFoundError, V as VersionNotFoundError, s as setLogLevel, d as LogLevel, H as HttpFetcher, F as FileFetcher, S as SearchTool, e as ScrapeTool, f as ListLibrariesTool, g as DEFAULT_PROTOCOL, h as DEFAULT_HTTP_PORT } from "./DocumentManagementService-_qCZ1Hi2.js";
|
|
5
|
+
import * as http from "node:http";
|
|
6
|
+
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
7
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
27
8
|
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
28
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
29
9
|
import { z } from "zod";
|
|
30
|
-
|
|
31
|
-
|
|
10
|
+
import "cheerio";
|
|
11
|
+
import "node:vm";
|
|
12
|
+
import "jsdom";
|
|
13
|
+
import "playwright";
|
|
14
|
+
import "@joplin/turndown-plugin-gfm";
|
|
15
|
+
import "turndown";
|
|
16
|
+
import "semver";
|
|
17
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
18
|
+
import { F as FetchUrlTool, a as FindVersionTool } from "./FindVersionTool-CH1c3Tyu.js";
|
|
19
|
+
import { R as RemoveTool, L as ListJobsTool } from "./RemoveTool-DmB1YJTA.js";
|
|
20
|
+
class CancelJobTool {
|
|
21
|
+
manager;
|
|
22
|
+
/**
|
|
23
|
+
* Creates an instance of CancelJobTool.
|
|
24
|
+
* @param manager The PipelineManager instance.
|
|
25
|
+
*/
|
|
26
|
+
constructor(manager) {
|
|
27
|
+
this.manager = manager;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Executes the tool to attempt cancellation of a specific job.
|
|
31
|
+
* @param input - The input parameters, containing the jobId.
|
|
32
|
+
* @returns A promise that resolves with the outcome message.
|
|
33
|
+
*/
|
|
34
|
+
async execute(input) {
|
|
35
|
+
try {
|
|
36
|
+
const job = await this.manager.getJob(input.jobId);
|
|
37
|
+
if (!job) {
|
|
38
|
+
logger.warn(`[CancelJobTool] Job not found: ${input.jobId}`);
|
|
39
|
+
return {
|
|
40
|
+
message: `Job with ID ${input.jobId} not found.`,
|
|
41
|
+
success: false
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (job.status === PipelineJobStatus.COMPLETED || // Use enum member
|
|
45
|
+
job.status === PipelineJobStatus.FAILED || // Use enum member
|
|
46
|
+
job.status === PipelineJobStatus.CANCELLED) {
|
|
47
|
+
logger.info(
|
|
48
|
+
`[CancelJobTool] Job ${input.jobId} is already in a final state: ${job.status}.`
|
|
49
|
+
);
|
|
50
|
+
return {
|
|
51
|
+
message: `Job ${input.jobId} is already ${job.status}. No action taken.`,
|
|
52
|
+
success: true
|
|
53
|
+
// Considered success as no cancellation needed
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
await this.manager.cancelJob(input.jobId);
|
|
57
|
+
const updatedJob = await this.manager.getJob(input.jobId);
|
|
58
|
+
const finalStatus = updatedJob?.status ?? "UNKNOWN (job disappeared?)";
|
|
59
|
+
logger.info(
|
|
60
|
+
`[CancelJobTool] Cancellation requested for job ${input.jobId}. Current status: ${finalStatus}`
|
|
61
|
+
);
|
|
62
|
+
return {
|
|
63
|
+
message: `Cancellation requested for job ${input.jobId}. Current status: ${finalStatus}.`,
|
|
64
|
+
success: true
|
|
65
|
+
};
|
|
66
|
+
} catch (error) {
|
|
67
|
+
logger.error(`[CancelJobTool] Error cancelling job ${input.jobId}: ${error}`);
|
|
68
|
+
return {
|
|
69
|
+
message: `Failed to cancel job ${input.jobId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
70
|
+
success: false
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
class GetJobInfoTool {
|
|
76
|
+
manager;
|
|
77
|
+
/**
|
|
78
|
+
* Creates an instance of GetJobInfoTool.
|
|
79
|
+
* @param manager The PipelineManager instance.
|
|
80
|
+
*/
|
|
81
|
+
constructor(manager) {
|
|
82
|
+
this.manager = manager;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Executes the tool to retrieve simplified info for a specific job.
|
|
86
|
+
* @param input - The input parameters, containing the jobId.
|
|
87
|
+
* @returns A promise that resolves with the simplified job info or null if not found.
|
|
88
|
+
*/
|
|
89
|
+
async execute(input) {
|
|
90
|
+
const job = await this.manager.getJob(input.jobId);
|
|
91
|
+
if (!job) {
|
|
92
|
+
return { job: null };
|
|
93
|
+
}
|
|
94
|
+
const jobInfo = {
|
|
95
|
+
id: job.id,
|
|
96
|
+
library: job.library,
|
|
97
|
+
version: job.version,
|
|
98
|
+
status: job.status,
|
|
99
|
+
createdAt: job.createdAt.toISOString(),
|
|
100
|
+
startedAt: job.startedAt?.toISOString() ?? null,
|
|
101
|
+
finishedAt: job.finishedAt?.toISOString() ?? null,
|
|
102
|
+
error: job.error?.message ?? null
|
|
103
|
+
};
|
|
104
|
+
return { job: jobInfo };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
let docService;
|
|
108
|
+
let pipelineManager;
|
|
109
|
+
async function initializeServices() {
|
|
110
|
+
if (docService || pipelineManager) {
|
|
111
|
+
logger.warn("Services already initialized.");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
docService = new DocumentManagementService();
|
|
115
|
+
try {
|
|
116
|
+
await docService.initialize();
|
|
117
|
+
logger.debug("DocumentManagementService initialized.");
|
|
118
|
+
pipelineManager = new PipelineManager(docService);
|
|
119
|
+
await pipelineManager.start();
|
|
120
|
+
logger.debug("PipelineManager initialized and started.");
|
|
121
|
+
} catch (error) {
|
|
122
|
+
logger.error(`Failed to initialize services: ${error}`);
|
|
123
|
+
await shutdownServices();
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function shutdownServices() {
|
|
128
|
+
if (pipelineManager) {
|
|
129
|
+
await pipelineManager.stop();
|
|
130
|
+
logger.info("PipelineManager stopped.");
|
|
131
|
+
pipelineManager = void 0;
|
|
132
|
+
}
|
|
133
|
+
if (docService) {
|
|
134
|
+
await docService.shutdown();
|
|
135
|
+
logger.info("DocumentManagementService shutdown.");
|
|
136
|
+
docService = void 0;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function getDocService() {
|
|
140
|
+
if (!docService) {
|
|
141
|
+
throw new Error("DocumentManagementService has not been initialized.");
|
|
142
|
+
}
|
|
143
|
+
return docService;
|
|
144
|
+
}
|
|
145
|
+
function getPipelineManager() {
|
|
146
|
+
if (!pipelineManager) {
|
|
147
|
+
throw new Error("PipelineManager has not been initialized.");
|
|
148
|
+
}
|
|
149
|
+
return pipelineManager;
|
|
150
|
+
}
|
|
32
151
|
function createResponse(text) {
|
|
33
152
|
return {
|
|
34
153
|
content: [
|
|
@@ -51,177 +170,177 @@ function createError(text) {
|
|
|
51
170
|
isError: true
|
|
52
171
|
};
|
|
53
172
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
findVersion: new FindVersionTool(docService),
|
|
66
|
-
scrape: new ScrapeTool(docService, pipelineManager),
|
|
67
|
-
search: new SearchTool(docService),
|
|
68
|
-
listJobs: new ListJobsTool(pipelineManager),
|
|
69
|
-
getJobInfo: new GetJobInfoTool(pipelineManager),
|
|
70
|
-
cancelJob: new CancelJobTool(pipelineManager),
|
|
71
|
-
remove: new RemoveTool(docService),
|
|
72
|
-
// FetchUrlTool now uses middleware pipeline internally
|
|
73
|
-
fetchUrl: new FetchUrlTool(new HttpFetcher(), new FileFetcher())
|
|
74
|
-
};
|
|
75
|
-
const server = new McpServer(
|
|
76
|
-
{
|
|
77
|
-
name: "docs-mcp-server",
|
|
78
|
-
version: "0.1.0"
|
|
79
|
-
},
|
|
80
|
-
{
|
|
81
|
-
capabilities: {
|
|
82
|
-
tools: {},
|
|
83
|
-
prompts: {},
|
|
84
|
-
resources: {}
|
|
85
|
-
}
|
|
173
|
+
function createMcpServerInstance(tools) {
|
|
174
|
+
const server = new McpServer(
|
|
175
|
+
{
|
|
176
|
+
name: "docs-mcp-server",
|
|
177
|
+
version: "0.1.0"
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
capabilities: {
|
|
181
|
+
tools: {},
|
|
182
|
+
prompts: {},
|
|
183
|
+
resources: {}
|
|
86
184
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
});
|
|
116
|
-
if ("jobId" in result) {
|
|
117
|
-
return createResponse(`\u{1F680} Scraping job started with ID: ${result.jobId}.`);
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
server.tool(
|
|
188
|
+
"scrape_docs",
|
|
189
|
+
"Scrape and index documentation from a URL",
|
|
190
|
+
{
|
|
191
|
+
url: z.string().url().describe("URL of the documentation to scrape"),
|
|
192
|
+
library: z.string().describe("Name of the library"),
|
|
193
|
+
version: z.string().optional().describe("Version of the library"),
|
|
194
|
+
maxPages: z.number().optional().default(DEFAULT_MAX_PAGES).describe(`Maximum number of pages to scrape (default: ${DEFAULT_MAX_PAGES})`),
|
|
195
|
+
maxDepth: z.number().optional().default(DEFAULT_MAX_DEPTH).describe(`Maximum navigation depth (default: ${DEFAULT_MAX_DEPTH})`),
|
|
196
|
+
scope: z.enum(["subpages", "hostname", "domain"]).optional().default("subpages").describe("Defines the crawling boundary: 'subpages', 'hostname', or 'domain'"),
|
|
197
|
+
followRedirects: z.boolean().optional().default(true).describe("Whether to follow HTTP redirects (3xx responses)")
|
|
198
|
+
},
|
|
199
|
+
async ({ url, library, version, maxPages, maxDepth, scope, followRedirects }) => {
|
|
200
|
+
try {
|
|
201
|
+
const result = await tools.scrape.execute({
|
|
202
|
+
url,
|
|
203
|
+
library,
|
|
204
|
+
version,
|
|
205
|
+
waitForCompletion: false,
|
|
206
|
+
// Don't wait for completion
|
|
207
|
+
// onProgress: undefined, // Explicitly undefined or omitted
|
|
208
|
+
options: {
|
|
209
|
+
maxPages,
|
|
210
|
+
maxDepth,
|
|
211
|
+
scope,
|
|
212
|
+
followRedirects
|
|
118
213
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
);
|
|
122
|
-
} catch (error) {
|
|
123
|
-
return createError(
|
|
124
|
-
`Failed to scrape documentation: ${error instanceof Error ? error.message : String(error)}`
|
|
125
|
-
);
|
|
214
|
+
});
|
|
215
|
+
if ("jobId" in result) {
|
|
216
|
+
return createResponse(`🚀 Scraping job started with ID: ${result.jobId}.`);
|
|
126
217
|
}
|
|
218
|
+
return createResponse(
|
|
219
|
+
`Scraping finished immediately (unexpectedly) with ${result.pagesScraped} pages.`
|
|
220
|
+
);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
return createError(
|
|
223
|
+
`Failed to scrape documentation: ${error instanceof Error ? error.message : String(error)}`
|
|
224
|
+
);
|
|
127
225
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
server.tool(
|
|
229
|
+
"search_docs",
|
|
230
|
+
'Searches up-to-date documentation for a library. Examples:\n\n- {library: "react", query: "hooks lifecycle"} -> matches latest version of React\n- {library: "react", version: "18.0.0", query: "hooks lifecycle"} -> matches React 18.0.0 or earlier\n- {library: "typescript", version: "5.x", query: "ReturnType example"} -> any TypeScript 5.x.x version\n- {library: "typescript", version: "5.2.x", query: "ReturnType example"} -> any TypeScript 5.2.x version',
|
|
231
|
+
{
|
|
232
|
+
library: z.string().describe("Name of the library"),
|
|
233
|
+
version: z.string().optional().describe(
|
|
234
|
+
"Version of the library (supports exact versions like '18.0.0' or X-Range patterns like '5.x', '5.2.x')"
|
|
235
|
+
),
|
|
236
|
+
query: z.string().describe("Search query"),
|
|
237
|
+
limit: z.number().optional().default(5).describe("Maximum number of results")
|
|
238
|
+
},
|
|
239
|
+
async ({ library, version, query, limit }) => {
|
|
240
|
+
try {
|
|
241
|
+
const result = await tools.search.execute({
|
|
242
|
+
library,
|
|
243
|
+
version,
|
|
244
|
+
query,
|
|
245
|
+
limit,
|
|
246
|
+
exactMatch: false
|
|
247
|
+
// Always false for MCP interface
|
|
248
|
+
});
|
|
249
|
+
const formattedResults = result.results.map(
|
|
250
|
+
(r, i) => `
|
|
152
251
|
------------------------------------------------------------
|
|
153
252
|
Result ${i + 1}: ${r.url}
|
|
154
253
|
|
|
155
254
|
${r.content}
|
|
156
255
|
`
|
|
157
|
-
|
|
256
|
+
);
|
|
257
|
+
if (formattedResults.length === 0) {
|
|
158
258
|
return createResponse(
|
|
159
|
-
`
|
|
160
|
-
${formattedResults.join("")}`
|
|
161
|
-
);
|
|
162
|
-
} catch (error) {
|
|
163
|
-
if (error instanceof VersionNotFoundError) {
|
|
164
|
-
const indexedVersions = error.availableVersions.filter((v) => v.indexed).map((v) => v.version);
|
|
165
|
-
return createError(
|
|
166
|
-
indexedVersions.length > 0 ? `Version not found. Available indexed versions for ${library}: ${indexedVersions.join(", ")}` : `Version not found. No indexed versions available for ${library}.`
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
return createError(
|
|
170
|
-
`Failed to search documentation: ${error instanceof Error ? error.message : String(error)}`
|
|
259
|
+
`No results found for '${query}' in ${library}. Try to use a different or more general query.`
|
|
171
260
|
);
|
|
172
261
|
}
|
|
173
|
-
}
|
|
174
|
-
);
|
|
175
|
-
server.tool("list_libraries", "List all indexed libraries", {}, async () => {
|
|
176
|
-
try {
|
|
177
|
-
const result = await tools.listLibraries.execute();
|
|
178
262
|
return createResponse(
|
|
179
|
-
`
|
|
180
|
-
${
|
|
263
|
+
`Search results for '${query}' in ${library}:
|
|
264
|
+
${formattedResults.join("")}`
|
|
181
265
|
);
|
|
182
266
|
} catch (error) {
|
|
267
|
+
if (error instanceof LibraryNotFoundError) {
|
|
268
|
+
return createResponse(
|
|
269
|
+
[
|
|
270
|
+
`Library "${library}" not found.`,
|
|
271
|
+
error.suggestions?.length ? `Did you mean: ${error.suggestions?.join(", ")}?` : void 0
|
|
272
|
+
].join(" ")
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
if (error instanceof VersionNotFoundError) {
|
|
276
|
+
const indexedVersions = error.availableVersions.map((v) => v.version);
|
|
277
|
+
return createResponse(
|
|
278
|
+
[
|
|
279
|
+
`Version "${version}" not found.`,
|
|
280
|
+
indexedVersions.length > 0 ? `Available indexed versions for ${library}: ${indexedVersions.join(", ")}` : void 0
|
|
281
|
+
].join(" ")
|
|
282
|
+
);
|
|
283
|
+
}
|
|
183
284
|
return createError(
|
|
184
|
-
`Failed to
|
|
285
|
+
`Failed to search documentation: ${error instanceof Error ? error.message : String(error)}`
|
|
185
286
|
);
|
|
186
287
|
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
288
|
+
}
|
|
289
|
+
);
|
|
290
|
+
server.tool("list_libraries", "List all indexed libraries", {}, async () => {
|
|
291
|
+
try {
|
|
292
|
+
const result = await tools.listLibraries.execute();
|
|
293
|
+
if (result.libraries.length === 0) {
|
|
294
|
+
return createResponse("No libraries indexed yet.");
|
|
295
|
+
}
|
|
296
|
+
return createResponse(
|
|
297
|
+
`Indexed libraries:
|
|
298
|
+
|
|
299
|
+
${result.libraries.map((lib) => `- ${lib.name}`).join("\n")}`
|
|
300
|
+
);
|
|
301
|
+
} catch (error) {
|
|
302
|
+
return createError(
|
|
303
|
+
`Failed to list libraries: ${error instanceof Error ? error.message : String(error)}`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
server.tool(
|
|
308
|
+
"find_version",
|
|
309
|
+
"Find best matching version for a library",
|
|
310
|
+
{
|
|
311
|
+
library: z.string().describe("Name of the library"),
|
|
312
|
+
targetVersion: z.string().optional().describe(
|
|
313
|
+
"Pattern to match (supports exact versions like '18.0.0' or X-Range patterns like '5.x', '5.2.x')"
|
|
314
|
+
)
|
|
315
|
+
},
|
|
316
|
+
async ({ library, targetVersion }) => {
|
|
317
|
+
try {
|
|
318
|
+
const message = await tools.findVersion.execute({
|
|
319
|
+
library,
|
|
320
|
+
targetVersion
|
|
321
|
+
});
|
|
322
|
+
if (!message) {
|
|
323
|
+
return createError("No matching version found");
|
|
211
324
|
}
|
|
325
|
+
return createResponse(message);
|
|
326
|
+
} catch (error) {
|
|
327
|
+
return createError(
|
|
328
|
+
`Failed to find version: ${error instanceof Error ? error.message : String(error)}`
|
|
329
|
+
);
|
|
212
330
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
331
|
+
}
|
|
332
|
+
);
|
|
333
|
+
server.tool(
|
|
334
|
+
"list_jobs",
|
|
335
|
+
"List pipeline jobs, optionally filtering by status.",
|
|
336
|
+
{
|
|
337
|
+
status: z.nativeEnum(PipelineJobStatus).optional().describe("Optional status to filter jobs by.")
|
|
338
|
+
},
|
|
339
|
+
async ({ status }) => {
|
|
340
|
+
try {
|
|
341
|
+
const result = await tools.listJobs.execute({ status });
|
|
342
|
+
const formattedJobs = result.jobs.map(
|
|
343
|
+
(job) => `- ID: ${job.id}
|
|
225
344
|
Status: ${job.status}
|
|
226
345
|
Library: ${job.library}
|
|
227
346
|
Version: ${job.version}
|
|
@@ -229,244 +348,421 @@ ${result.libraries.map((lib) => `- ${lib.name}`).join("\n")}`
|
|
|
229
348
|
Started: ${job.startedAt}` : ""}${job.finishedAt ? `
|
|
230
349
|
Finished: ${job.finishedAt}` : ""}${job.error ? `
|
|
231
350
|
Error: ${job.error}` : ""}`
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
351
|
+
).join("\n\n");
|
|
352
|
+
return createResponse(
|
|
353
|
+
result.jobs.length > 0 ? `Current Jobs:
|
|
235
354
|
|
|
236
|
-
${formattedJobs}` : "No jobs found
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
355
|
+
${formattedJobs}` : "No jobs found."
|
|
356
|
+
);
|
|
357
|
+
} catch (error) {
|
|
358
|
+
return createError(
|
|
359
|
+
`Failed to list jobs: ${error instanceof Error ? error.message : String(error)}`
|
|
360
|
+
);
|
|
243
361
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
|
|
362
|
+
}
|
|
363
|
+
);
|
|
364
|
+
server.tool(
|
|
365
|
+
"get_job_info",
|
|
366
|
+
"Get the simplified info for a specific pipeline job.",
|
|
367
|
+
{
|
|
368
|
+
jobId: z.string().uuid().describe("The ID of the job to query.")
|
|
369
|
+
},
|
|
370
|
+
async ({ jobId }) => {
|
|
371
|
+
try {
|
|
372
|
+
const result = await tools.getJobInfo.execute({ jobId });
|
|
373
|
+
if (!result.job) {
|
|
374
|
+
return createError(`Job with ID ${jobId} not found.`);
|
|
375
|
+
}
|
|
376
|
+
const job = result.job;
|
|
377
|
+
const formattedJob = `- ID: ${job.id}
|
|
259
378
|
Status: ${job.status}
|
|
260
379
|
Library: ${job.library}@${job.version}
|
|
261
380
|
Created: ${job.createdAt}${job.startedAt ? `
|
|
262
381
|
Started: ${job.startedAt}` : ""}${job.finishedAt ? `
|
|
263
382
|
Finished: ${job.finishedAt}` : ""}${job.error ? `
|
|
264
383
|
Error: ${job.error}` : ""}`;
|
|
265
|
-
|
|
384
|
+
return createResponse(`Job Info:
|
|
266
385
|
|
|
267
386
|
${formattedJob}`);
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
);
|
|
275
|
-
server.tool(
|
|
276
|
-
"fetch_url",
|
|
277
|
-
"Fetch a single URL and convert its content to Markdown",
|
|
278
|
-
{
|
|
279
|
-
url: z.string().url().describe("The URL to fetch and convert to markdown"),
|
|
280
|
-
followRedirects: z.boolean().optional().default(true).describe("Whether to follow HTTP redirects (3xx responses)")
|
|
281
|
-
},
|
|
282
|
-
async ({ url, followRedirects }) => {
|
|
283
|
-
try {
|
|
284
|
-
const result = await tools.fetchUrl.execute({ url, followRedirects });
|
|
285
|
-
return createResponse(result);
|
|
286
|
-
} catch (error) {
|
|
287
|
-
return createError(
|
|
288
|
-
`Failed to fetch URL: ${error instanceof Error ? error.message : String(error)}`
|
|
289
|
-
);
|
|
290
|
-
}
|
|
387
|
+
} catch (error) {
|
|
388
|
+
return createError(
|
|
389
|
+
`Failed to get job info for ${jobId}: ${error instanceof Error ? error.message : String(error)}`
|
|
390
|
+
);
|
|
291
391
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
);
|
|
310
|
-
}
|
|
392
|
+
}
|
|
393
|
+
);
|
|
394
|
+
server.tool(
|
|
395
|
+
"fetch_url",
|
|
396
|
+
"Fetch a single URL and convert its content to Markdown",
|
|
397
|
+
{
|
|
398
|
+
url: z.string().url().describe("The URL to fetch and convert to markdown"),
|
|
399
|
+
followRedirects: z.boolean().optional().default(true).describe("Whether to follow HTTP redirects (3xx responses)")
|
|
400
|
+
},
|
|
401
|
+
async ({ url, followRedirects }) => {
|
|
402
|
+
try {
|
|
403
|
+
const result = await tools.fetchUrl.execute({ url, followRedirects });
|
|
404
|
+
return createResponse(result);
|
|
405
|
+
} catch (error) {
|
|
406
|
+
return createError(
|
|
407
|
+
`Failed to fetch URL: ${error instanceof Error ? error.message : String(error)}`
|
|
408
|
+
);
|
|
311
409
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
410
|
+
}
|
|
411
|
+
);
|
|
412
|
+
server.tool(
|
|
413
|
+
"cancel_job",
|
|
414
|
+
"Attempt to cancel a queued or running pipeline job.",
|
|
415
|
+
{
|
|
416
|
+
jobId: z.string().uuid().describe("The ID of the job to cancel.")
|
|
417
|
+
},
|
|
418
|
+
async ({ jobId }) => {
|
|
419
|
+
try {
|
|
420
|
+
const result = await tools.cancelJob.execute({ jobId });
|
|
421
|
+
if (result.success) {
|
|
323
422
|
return createResponse(result.message);
|
|
324
|
-
} catch (error) {
|
|
325
|
-
return createError(
|
|
326
|
-
`Failed to remove documents: ${error instanceof Error ? error.message : String(error)}`
|
|
327
|
-
);
|
|
328
423
|
}
|
|
424
|
+
return createError(result.message);
|
|
425
|
+
} catch (error) {
|
|
426
|
+
return createError(
|
|
427
|
+
`Failed to cancel job ${jobId}: ${error instanceof Error ? error.message : String(error)}`
|
|
428
|
+
);
|
|
329
429
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
]
|
|
350
|
-
};
|
|
430
|
+
}
|
|
431
|
+
);
|
|
432
|
+
server.tool(
|
|
433
|
+
"remove_docs",
|
|
434
|
+
"Remove indexed documentation for a library version.",
|
|
435
|
+
{
|
|
436
|
+
library: z.string().describe("Name of the library"),
|
|
437
|
+
version: z.string().optional().describe("Version of the library (optional, removes unversioned if omitted)")
|
|
438
|
+
},
|
|
439
|
+
async ({ library, version }) => {
|
|
440
|
+
try {
|
|
441
|
+
const result = await tools.remove.execute({ library, version });
|
|
442
|
+
return createResponse(result.message);
|
|
443
|
+
} catch (error) {
|
|
444
|
+
return createError(
|
|
445
|
+
`Failed to remove documents: ${error instanceof Error ? error.message : String(error)}`
|
|
446
|
+
);
|
|
351
447
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
448
|
+
}
|
|
449
|
+
);
|
|
450
|
+
server.prompt(
|
|
451
|
+
"docs",
|
|
452
|
+
"Search indexed documentation",
|
|
453
|
+
{
|
|
454
|
+
library: z.string().describe("Name of the library"),
|
|
455
|
+
version: z.string().optional().describe("Version of the library"),
|
|
456
|
+
query: z.string().describe("Search query")
|
|
457
|
+
},
|
|
458
|
+
async ({ library, version, query }) => {
|
|
459
|
+
return {
|
|
460
|
+
messages: [
|
|
461
|
+
{
|
|
462
|
+
role: "user",
|
|
463
|
+
content: {
|
|
464
|
+
type: "text",
|
|
465
|
+
text: `Please search ${library} ${version || ""} documentation for this query: ${query}`
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
]
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
);
|
|
472
|
+
server.resource(
|
|
473
|
+
"libraries",
|
|
474
|
+
"docs://libraries",
|
|
475
|
+
{
|
|
476
|
+
description: "List all indexed libraries"
|
|
477
|
+
},
|
|
478
|
+
async (uri) => {
|
|
479
|
+
const result = await tools.listLibraries.execute();
|
|
480
|
+
return {
|
|
481
|
+
contents: result.libraries.map((lib) => ({
|
|
482
|
+
uri: new URL(lib.name, uri).href,
|
|
483
|
+
text: lib.name
|
|
484
|
+
}))
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
);
|
|
488
|
+
server.resource(
|
|
489
|
+
"versions",
|
|
490
|
+
new ResourceTemplate("docs://libraries/{library}/versions", {
|
|
491
|
+
list: void 0
|
|
492
|
+
}),
|
|
493
|
+
{
|
|
494
|
+
description: "List all indexed versions for a library"
|
|
495
|
+
},
|
|
496
|
+
async (uri, { library }) => {
|
|
497
|
+
const result = await tools.listLibraries.execute();
|
|
498
|
+
const lib = result.libraries.find((l) => l.name === library);
|
|
499
|
+
if (!lib) {
|
|
500
|
+
return { contents: [] };
|
|
367
501
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
502
|
+
return {
|
|
503
|
+
contents: lib.versions.map((v) => ({
|
|
504
|
+
uri: new URL(v.version, uri).href,
|
|
505
|
+
text: v.version
|
|
506
|
+
}))
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
);
|
|
510
|
+
server.resource(
|
|
511
|
+
"jobs",
|
|
512
|
+
"docs://jobs",
|
|
513
|
+
{
|
|
514
|
+
description: "List pipeline jobs, optionally filtering by status.",
|
|
515
|
+
mimeType: "application/json"
|
|
516
|
+
},
|
|
517
|
+
async (uri) => {
|
|
518
|
+
const statusParam = uri.searchParams.get("status");
|
|
519
|
+
let statusFilter;
|
|
520
|
+
if (statusParam) {
|
|
521
|
+
const validation = z.nativeEnum(PipelineJobStatus).safeParse(statusParam);
|
|
522
|
+
if (validation.success) {
|
|
523
|
+
statusFilter = validation.data;
|
|
524
|
+
} else {
|
|
525
|
+
logger.warn(`Invalid status parameter received: ${statusParam}`);
|
|
382
526
|
}
|
|
383
|
-
return {
|
|
384
|
-
contents: lib.versions.map((v) => ({
|
|
385
|
-
uri: new URL(v.version, uri).href,
|
|
386
|
-
text: v.version
|
|
387
|
-
}))
|
|
388
|
-
};
|
|
389
527
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
async (uri) => {
|
|
399
|
-
const statusParam = uri.searchParams.get("status");
|
|
400
|
-
let statusFilter;
|
|
401
|
-
if (statusParam) {
|
|
402
|
-
const validation = z.nativeEnum(PipelineJobStatus).safeParse(statusParam);
|
|
403
|
-
if (validation.success) {
|
|
404
|
-
statusFilter = validation.data;
|
|
405
|
-
} else {
|
|
406
|
-
logger.warn(`Invalid status parameter received: ${statusParam}`);
|
|
528
|
+
const result = await tools.listJobs.execute({ status: statusFilter });
|
|
529
|
+
return {
|
|
530
|
+
contents: [
|
|
531
|
+
{
|
|
532
|
+
uri: uri.href,
|
|
533
|
+
mimeType: "application/json",
|
|
534
|
+
text: JSON.stringify(result.jobs, null, 2)
|
|
535
|
+
// Stringify the simplified jobs array
|
|
407
536
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
537
|
+
]
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
);
|
|
541
|
+
server.resource(
|
|
542
|
+
"job",
|
|
543
|
+
// A distinct name for this specific resource type
|
|
544
|
+
new ResourceTemplate("docs://jobs/{jobId}", { list: void 0 }),
|
|
545
|
+
{
|
|
546
|
+
description: "Get details for a specific pipeline job by ID.",
|
|
547
|
+
mimeType: "application/json"
|
|
548
|
+
},
|
|
549
|
+
async (uri, { jobId }) => {
|
|
550
|
+
if (typeof jobId !== "string" || jobId.length === 0) {
|
|
551
|
+
logger.warn(`Invalid jobId received in URI: ${jobId}`);
|
|
552
|
+
return { contents: [] };
|
|
420
553
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
554
|
+
const result = await tools.getJobInfo.execute({ jobId });
|
|
555
|
+
if (!result.job) {
|
|
556
|
+
return { contents: [] };
|
|
557
|
+
}
|
|
558
|
+
return {
|
|
559
|
+
contents: [
|
|
560
|
+
{
|
|
561
|
+
uri: uri.href,
|
|
562
|
+
mimeType: "application/json",
|
|
563
|
+
text: JSON.stringify(result.job, null, 2)
|
|
564
|
+
// Stringify the simplified job object
|
|
565
|
+
}
|
|
566
|
+
]
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
);
|
|
570
|
+
return server;
|
|
571
|
+
}
|
|
572
|
+
async function startHttpServer(tools, port) {
|
|
573
|
+
setLogLevel(LogLevel.INFO);
|
|
574
|
+
const server = createMcpServerInstance(tools);
|
|
575
|
+
const sseTransports = {};
|
|
576
|
+
const httpServer = http.createServer(async (req, res) => {
|
|
577
|
+
try {
|
|
578
|
+
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
579
|
+
if (req.method === "GET" && url.pathname === "/sse") {
|
|
580
|
+
const transport = new SSEServerTransport("/messages", res);
|
|
581
|
+
sseTransports[transport.sessionId] = transport;
|
|
582
|
+
res.on("close", () => {
|
|
583
|
+
delete sseTransports[transport.sessionId];
|
|
584
|
+
transport.close();
|
|
585
|
+
});
|
|
586
|
+
await server.connect(transport);
|
|
587
|
+
} else if (req.method === "POST" && url.pathname === "/messages") {
|
|
588
|
+
const sessionId = url.searchParams.get("sessionId");
|
|
589
|
+
const transport = sessionId ? sseTransports[sessionId] : void 0;
|
|
590
|
+
if (transport) {
|
|
591
|
+
let body = "";
|
|
592
|
+
for await (const chunk of req) {
|
|
593
|
+
body += chunk;
|
|
594
|
+
}
|
|
595
|
+
const parsedBody = JSON.parse(body);
|
|
596
|
+
await transport.handlePostMessage(req, res, parsedBody);
|
|
597
|
+
} else {
|
|
598
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
599
|
+
res.end(JSON.stringify({ error: "No transport found for sessionId" }));
|
|
434
600
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
601
|
+
} else if (url.pathname === "/mcp") {
|
|
602
|
+
let body = "";
|
|
603
|
+
for await (const chunk of req) {
|
|
604
|
+
body += chunk;
|
|
438
605
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
};
|
|
606
|
+
const parsedBody = JSON.parse(body);
|
|
607
|
+
const requestServer = createMcpServerInstance(tools);
|
|
608
|
+
const requestTransport = new StreamableHTTPServerTransport({
|
|
609
|
+
sessionIdGenerator: void 0
|
|
610
|
+
});
|
|
611
|
+
res.on("close", () => {
|
|
612
|
+
logger.info("Streamable HTTP request closed");
|
|
613
|
+
requestTransport.close();
|
|
614
|
+
requestServer.close();
|
|
615
|
+
});
|
|
616
|
+
await requestServer.connect(requestTransport);
|
|
617
|
+
await requestTransport.handleRequest(req, res, parsedBody);
|
|
618
|
+
} else {
|
|
619
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
620
|
+
res.end(
|
|
621
|
+
JSON.stringify({
|
|
622
|
+
error: `Endpoint ${url.pathname} not found.`
|
|
623
|
+
})
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
} catch (error) {
|
|
627
|
+
logger.error(`Error handling HTTP request: ${error}`);
|
|
628
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
629
|
+
res.end(
|
|
630
|
+
JSON.stringify({
|
|
631
|
+
error: error instanceof Error ? error.message : String(error)
|
|
632
|
+
})
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
httpServer.listen(port, () => {
|
|
637
|
+
logger.info(`🤖 Docs MCP server listening at http://127.0.0.1:${port}`);
|
|
638
|
+
});
|
|
639
|
+
return server;
|
|
640
|
+
}
|
|
641
|
+
async function startStdioServer(tools) {
|
|
642
|
+
setLogLevel(LogLevel.ERROR);
|
|
643
|
+
const server = createMcpServerInstance(tools);
|
|
644
|
+
const transport = new StdioServerTransport();
|
|
645
|
+
await server.connect(transport);
|
|
646
|
+
logger.info("🤖 Docs MCP server listening on stdio");
|
|
647
|
+
return server;
|
|
648
|
+
}
|
|
649
|
+
async function initializeTools() {
|
|
650
|
+
const docService2 = getDocService();
|
|
651
|
+
const pipelineManager2 = getPipelineManager();
|
|
652
|
+
const tools = {
|
|
653
|
+
listLibraries: new ListLibrariesTool(docService2),
|
|
654
|
+
findVersion: new FindVersionTool(docService2),
|
|
655
|
+
scrape: new ScrapeTool(docService2, pipelineManager2),
|
|
656
|
+
search: new SearchTool(docService2),
|
|
657
|
+
listJobs: new ListJobsTool(pipelineManager2),
|
|
658
|
+
getJobInfo: new GetJobInfoTool(pipelineManager2),
|
|
659
|
+
cancelJob: new CancelJobTool(pipelineManager2),
|
|
660
|
+
remove: new RemoveTool(docService2),
|
|
661
|
+
// FetchUrlTool now uses middleware pipeline internally
|
|
662
|
+
fetchUrl: new FetchUrlTool(new HttpFetcher(), new FileFetcher())
|
|
663
|
+
};
|
|
664
|
+
return tools;
|
|
665
|
+
}
|
|
666
|
+
let runningServer = null;
|
|
667
|
+
let runningPipelineManager = null;
|
|
668
|
+
let runningDocService = null;
|
|
669
|
+
async function startServer(protocol, port) {
|
|
670
|
+
try {
|
|
671
|
+
setLogLevel(LogLevel.ERROR);
|
|
672
|
+
await initializeServices();
|
|
673
|
+
runningDocService = getDocService();
|
|
674
|
+
runningPipelineManager = getPipelineManager();
|
|
675
|
+
const tools = await initializeTools();
|
|
676
|
+
let serverInstance;
|
|
677
|
+
if (protocol === "stdio") {
|
|
678
|
+
serverInstance = await startStdioServer(tools);
|
|
679
|
+
} else if (protocol === "http") {
|
|
680
|
+
if (port === void 0) {
|
|
681
|
+
logger.error("HTTP protocol requires a port.");
|
|
682
|
+
process.exit(1);
|
|
449
683
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
684
|
+
serverInstance = await startHttpServer(tools, port);
|
|
685
|
+
} else {
|
|
686
|
+
logger.error(`Unknown protocol: ${protocol}`);
|
|
687
|
+
process.exit(1);
|
|
688
|
+
}
|
|
689
|
+
runningServer = serverInstance;
|
|
454
690
|
process.on("SIGINT", async () => {
|
|
455
|
-
|
|
456
|
-
await
|
|
457
|
-
await server.close();
|
|
691
|
+
logger.info("Received SIGINT. Shutting down gracefully...");
|
|
692
|
+
await stopServer();
|
|
458
693
|
process.exit(0);
|
|
459
694
|
});
|
|
460
695
|
} catch (error) {
|
|
461
|
-
|
|
462
|
-
|
|
696
|
+
logger.error(`❌ Fatal Error during server startup: ${error}`);
|
|
697
|
+
await stopServer();
|
|
463
698
|
process.exit(1);
|
|
464
699
|
}
|
|
465
700
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
701
|
+
async function stopServer() {
|
|
702
|
+
logger.info("Shutting down MCP server and services...");
|
|
703
|
+
let hadError = false;
|
|
704
|
+
try {
|
|
705
|
+
if (runningPipelineManager) {
|
|
706
|
+
logger.debug("Stopping Pipeline Manager...");
|
|
707
|
+
await runningPipelineManager.stop();
|
|
708
|
+
logger.info("Pipeline Manager stopped.");
|
|
709
|
+
}
|
|
710
|
+
} catch (e) {
|
|
711
|
+
logger.error(`Error stopping Pipeline Manager: ${e}`);
|
|
712
|
+
hadError = true;
|
|
713
|
+
}
|
|
714
|
+
try {
|
|
715
|
+
if (runningDocService) {
|
|
716
|
+
logger.debug("Shutting down Document Service...");
|
|
717
|
+
await runningDocService.shutdown();
|
|
718
|
+
logger.info("Document Service shut down.");
|
|
719
|
+
}
|
|
720
|
+
} catch (e) {
|
|
721
|
+
logger.error(`Error shutting down Document Service: ${e}`);
|
|
722
|
+
hadError = true;
|
|
723
|
+
}
|
|
724
|
+
try {
|
|
725
|
+
if (runningServer) {
|
|
726
|
+
logger.debug("Closing MCP Server connection...");
|
|
727
|
+
await runningServer.close();
|
|
728
|
+
logger.info("MCP Server connection closed.");
|
|
729
|
+
}
|
|
730
|
+
} catch (e) {
|
|
731
|
+
logger.error(`Error closing MCP Server: ${e}`);
|
|
732
|
+
hadError = true;
|
|
733
|
+
}
|
|
734
|
+
runningPipelineManager = null;
|
|
735
|
+
runningDocService = null;
|
|
736
|
+
runningServer = null;
|
|
737
|
+
if (hadError) {
|
|
738
|
+
logger.warn("Server shutdown completed with errors.");
|
|
739
|
+
} else {
|
|
740
|
+
logger.info("✅ Server shutdown complete.");
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
program.option("--protocol <type>", "Protocol to use (stdio or http)", DEFAULT_PROTOCOL).option(
|
|
744
|
+
"--port <number>",
|
|
745
|
+
"Port to listen on for http protocol",
|
|
746
|
+
`${DEFAULT_HTTP_PORT}`
|
|
747
|
+
).parse(process.argv);
|
|
748
|
+
const options = program.opts();
|
|
749
|
+
async function main() {
|
|
750
|
+
const protocol = options.protocol;
|
|
751
|
+
const port = process.env.MCP_PORT ? Number.parseInt(process.env.MCP_PORT, 10) : Number.parseInt(options.port, 10);
|
|
752
|
+
if (protocol !== "stdio" && protocol !== "http") {
|
|
753
|
+
console.error('Invalid protocol specified. Use "stdio" or "http".');
|
|
754
|
+
process.exit(1);
|
|
755
|
+
}
|
|
756
|
+
if (protocol === "http" && Number.isNaN(port)) {
|
|
757
|
+
console.error("Port must be a number when using http protocol.");
|
|
758
|
+
process.exit(1);
|
|
759
|
+
}
|
|
760
|
+
try {
|
|
761
|
+
await startServer(protocol, protocol === "http" ? port : void 0);
|
|
762
|
+
} catch (error) {
|
|
763
|
+
console.error(`Server failed to start: ${error}`);
|
|
764
|
+
process.exit(1);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
main();
|
|
768
|
+
//# sourceMappingURL=server.js.map
|