http-client-mcp-server 1.0.0 → 1.1.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 +1 -1
- package/index.js +289 -23
- package/index.ts +349 -30
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# http-client-mcp-server
|
|
2
2
|
|
|
3
3
|
A Model Context Protocol (MCP) server that provides a set of tools to explore and read files from a repository. This is specifically designed to help AI agents understand and navigate large codebases.
|
|
4
4
|
|
package/index.js
CHANGED
|
@@ -4,25 +4,131 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import fs from "node:fs/promises";
|
|
6
6
|
import path from "node:path";
|
|
7
|
+
import yaml from "js-yaml";
|
|
8
|
+
const IGNORED_DIRS = [
|
|
9
|
+
"node_modules",
|
|
10
|
+
".git",
|
|
11
|
+
".idea",
|
|
12
|
+
".vscode",
|
|
13
|
+
"dist",
|
|
14
|
+
"build",
|
|
15
|
+
".next",
|
|
16
|
+
".cache",
|
|
17
|
+
];
|
|
18
|
+
const MAX_SEARCH_RESULTS = 50;
|
|
19
|
+
const MAX_LIST_ENTRIES = 100;
|
|
20
|
+
const MAX_FILE_SIZE_BYTES = 100 * 1024; // 100KB
|
|
21
|
+
function pruneOpenApiObject(obj) {
|
|
22
|
+
if (!obj || typeof obj !== "object")
|
|
23
|
+
return obj;
|
|
24
|
+
if (Array.isArray(obj)) {
|
|
25
|
+
return obj.map(pruneOpenApiObject);
|
|
26
|
+
}
|
|
27
|
+
const pruned = {};
|
|
28
|
+
const skipKeys = ["description", "summary", "example", "examples"];
|
|
29
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
30
|
+
if (skipKeys.includes(key) || key.startsWith("x-")) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
pruned[key] = pruneOpenApiObject(value);
|
|
34
|
+
}
|
|
35
|
+
return pruned;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Minifies JSON code blocks in Markdown.
|
|
39
|
+
*/
|
|
40
|
+
function minifyJsonInMarkdown(text) {
|
|
41
|
+
return text.replace(/```json([\s\S]*?)```/g, (match, jsonStr) => {
|
|
42
|
+
try {
|
|
43
|
+
const minified = JSON.stringify(JSON.parse(jsonStr.trim()));
|
|
44
|
+
return `\`\`\`json\n${minified}\n\`\`\``;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return match;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Flattens Markdown tables into concise bulleted lists.
|
|
53
|
+
*/
|
|
54
|
+
function flattenMarkdownTables(text) {
|
|
55
|
+
const lines = text.split("\n");
|
|
56
|
+
const result = [];
|
|
57
|
+
let inTable = false;
|
|
58
|
+
let headers = [];
|
|
59
|
+
for (const line of lines) {
|
|
60
|
+
const isDivider = /^[\s|:-]+$/.test(line);
|
|
61
|
+
const isRow = line.trim().startsWith("|") && line.trim().endsWith("|");
|
|
62
|
+
if (isRow && !isDivider) {
|
|
63
|
+
const cells = line
|
|
64
|
+
.split("|")
|
|
65
|
+
.map((c) => c.trim())
|
|
66
|
+
.filter((c, i, arr) => i > 0 && i < arr.length - 1);
|
|
67
|
+
if (!inTable) {
|
|
68
|
+
inTable = true;
|
|
69
|
+
headers = cells;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
// It's a data row
|
|
73
|
+
if (cells.length > 0) {
|
|
74
|
+
const formatted = cells
|
|
75
|
+
.map((cell, i) => {
|
|
76
|
+
const header = headers[i] || `col${i}`;
|
|
77
|
+
return cell ? `${header}: ${cell}` : "";
|
|
78
|
+
})
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
.join(", ");
|
|
81
|
+
result.push(`- ${formatted}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
if (inTable && !isDivider && !isRow) {
|
|
86
|
+
inTable = false;
|
|
87
|
+
}
|
|
88
|
+
if (!isDivider) {
|
|
89
|
+
result.push(line);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return result.join("\n");
|
|
94
|
+
}
|
|
95
|
+
function optimizeMarkdown(text) {
|
|
96
|
+
let optimized = text;
|
|
97
|
+
optimized = optimized.replace(/-- no response --/gi, "");
|
|
98
|
+
optimized = optimized.replace(/Content-Type: application\/json/gi, "");
|
|
99
|
+
optimized = flattenMarkdownTables(optimized);
|
|
100
|
+
optimized = minifyJsonInMarkdown(optimized);
|
|
101
|
+
const seenBlocks = new Set();
|
|
102
|
+
optimized = optimized.replace(/```[\s\S]*?```/g, (match) => {
|
|
103
|
+
const cleaned = match.replace(/\s/g, ""); // canonicalize
|
|
104
|
+
if (seenBlocks.has(cleaned) && cleaned.length > 50) {
|
|
105
|
+
return "[Same body as above]";
|
|
106
|
+
}
|
|
107
|
+
seenBlocks.add(cleaned);
|
|
108
|
+
return match;
|
|
109
|
+
});
|
|
110
|
+
optimized = optimized.replace(/\n\s*\n\s*\n+/g, "\n\n");
|
|
111
|
+
return optimized.trim();
|
|
112
|
+
}
|
|
7
113
|
const rawRepoPath = process.env.REPO_BASE_PATH || process.argv[2];
|
|
8
114
|
if (!rawRepoPath) {
|
|
9
115
|
const currentPath = process.cwd();
|
|
10
116
|
console.error("Error: REPO_BASE_PATH environment variable or a command-line argument is required.");
|
|
11
117
|
console.error(`\nHint: If you want to use the current directory, the absolute path is:\n${currentPath}`);
|
|
12
|
-
console.error(`\nUsage example:\
|
|
118
|
+
console.error(`\nUsage example:\nhttp-client-mcp-server ${currentPath}`);
|
|
13
119
|
console.error("\nTips to find your repository path:");
|
|
14
120
|
console.error("- macOS: Right-click folder + hold 'Option' key -> Select 'Copy as Pathname'");
|
|
15
121
|
console.error("- Windows: Shift + Right-click folder -> Select 'Copy as path'");
|
|
16
122
|
process.exit(1);
|
|
17
123
|
}
|
|
18
124
|
const REPO_BASE_PATH = path.resolve(rawRepoPath);
|
|
19
|
-
const server = new Server({ name: "
|
|
125
|
+
const server = new Server({ name: "http-client-mcp-server", version: "1.0.0" }, { capabilities: { tools: {} } });
|
|
20
126
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
21
127
|
return {
|
|
22
128
|
tools: [
|
|
23
129
|
{
|
|
24
130
|
name: "read_api_spec",
|
|
25
|
-
description: "Reads API specifications,
|
|
131
|
+
description: "Reads API specifications, source code, or any files. Supports line ranges for large files.",
|
|
26
132
|
inputSchema: {
|
|
27
133
|
type: "object",
|
|
28
134
|
properties: {
|
|
@@ -30,13 +136,43 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
30
136
|
type: "string",
|
|
31
137
|
description: "Relative path to the file inside the repo",
|
|
32
138
|
},
|
|
139
|
+
startLine: {
|
|
140
|
+
type: "number",
|
|
141
|
+
description: "Optional start line number (1-based)",
|
|
142
|
+
},
|
|
143
|
+
endLine: {
|
|
144
|
+
type: "number",
|
|
145
|
+
description: "Optional end line number (1-based)",
|
|
146
|
+
},
|
|
33
147
|
},
|
|
34
148
|
required: ["filepath"],
|
|
35
149
|
},
|
|
36
150
|
},
|
|
151
|
+
{
|
|
152
|
+
name: "read_specific_endpoint",
|
|
153
|
+
description: "Reads a specific endpoint and method from an OpenAPI spec (YAML/JSON) or Markdown doc. Optimizes output to save tokens.",
|
|
154
|
+
inputSchema: {
|
|
155
|
+
type: "object",
|
|
156
|
+
properties: {
|
|
157
|
+
filepath: {
|
|
158
|
+
type: "string",
|
|
159
|
+
description: "Relative path to the YAML/JSON/MD spec file inside the repo",
|
|
160
|
+
},
|
|
161
|
+
endpointPath: {
|
|
162
|
+
type: "string",
|
|
163
|
+
description: "The API path (e.g., '/v1/account/work-information')",
|
|
164
|
+
},
|
|
165
|
+
method: {
|
|
166
|
+
type: "string",
|
|
167
|
+
description: "The HTTP method (e.g., 'get', 'post', 'put', 'delete')",
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
required: ["filepath", "endpointPath", "method"],
|
|
171
|
+
},
|
|
172
|
+
},
|
|
37
173
|
{
|
|
38
174
|
name: "list_directory",
|
|
39
|
-
description: "List files and folders in a specific directory
|
|
175
|
+
description: "List files and folders in a specific directory. Limits output to prevent context blowout.",
|
|
40
176
|
inputSchema: {
|
|
41
177
|
type: "object",
|
|
42
178
|
properties: {
|
|
@@ -50,13 +186,13 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
50
186
|
},
|
|
51
187
|
{
|
|
52
188
|
name: "search_files",
|
|
53
|
-
description: "Search for files across the
|
|
189
|
+
description: "Search for files across the repo using a keyword. Limits results to prevent context blowout.",
|
|
54
190
|
inputSchema: {
|
|
55
191
|
type: "object",
|
|
56
192
|
properties: {
|
|
57
193
|
keyword: {
|
|
58
194
|
type: "string",
|
|
59
|
-
description: "Keyword to search for in filenames (e.g., 'update-address'
|
|
195
|
+
description: "Keyword to search for in filenames (e.g., 'update-address')",
|
|
60
196
|
},
|
|
61
197
|
},
|
|
62
198
|
required: ["keyword"],
|
|
@@ -66,19 +202,37 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
66
202
|
};
|
|
67
203
|
});
|
|
68
204
|
async function handleReadApiSpec(args) {
|
|
69
|
-
const filepath
|
|
205
|
+
const { filepath, startLine, endLine } = args;
|
|
70
206
|
if (!filepath) {
|
|
71
|
-
throw new Error("filepath is required
|
|
207
|
+
throw new Error("filepath is required");
|
|
72
208
|
}
|
|
73
209
|
const fullPath = path.resolve(REPO_BASE_PATH, filepath);
|
|
74
210
|
if (!fullPath.startsWith(REPO_BASE_PATH)) {
|
|
75
|
-
console.error(`[Security Warning] Attempted path traversal: ${filepath}`);
|
|
76
211
|
throw new Error("Access denied: File is outside the repository base path.");
|
|
77
212
|
}
|
|
78
213
|
try {
|
|
214
|
+
const stats = await fs.stat(fullPath);
|
|
215
|
+
if (stats.size > MAX_FILE_SIZE_BYTES && !startLine && !endLine) {
|
|
216
|
+
return {
|
|
217
|
+
content: [
|
|
218
|
+
{
|
|
219
|
+
type: "text",
|
|
220
|
+
text: `File is too large (${Math.round(stats.size / 1024)}KB). Please use 'read_specific_endpoint' for API specs or specify 'startLine' and 'endLine' to read specific segments.`,
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
isError: true,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
79
226
|
const content = await fs.readFile(fullPath, "utf-8");
|
|
227
|
+
let resultText = content;
|
|
228
|
+
if (startLine || endLine) {
|
|
229
|
+
const lines = content.split("\n");
|
|
230
|
+
const start = startLine ? Math.max(0, startLine - 1) : 0;
|
|
231
|
+
const end = endLine ? Math.min(lines.length, endLine) : lines.length;
|
|
232
|
+
resultText = lines.slice(start, end).join("\n");
|
|
233
|
+
}
|
|
80
234
|
return {
|
|
81
|
-
content: [{ type: "text", text:
|
|
235
|
+
content: [{ type: "text", text: resultText }],
|
|
82
236
|
};
|
|
83
237
|
}
|
|
84
238
|
catch (error) {
|
|
@@ -88,6 +242,102 @@ async function handleReadApiSpec(args) {
|
|
|
88
242
|
};
|
|
89
243
|
}
|
|
90
244
|
}
|
|
245
|
+
async function handleReadSpecificEndpoint(args) {
|
|
246
|
+
const { filepath, endpointPath, method } = args;
|
|
247
|
+
if (!filepath || !endpointPath || !method) {
|
|
248
|
+
throw new Error("filepath, endpointPath, and method are required");
|
|
249
|
+
}
|
|
250
|
+
const fullPath = path.resolve(REPO_BASE_PATH, filepath);
|
|
251
|
+
if (!fullPath.startsWith(REPO_BASE_PATH)) {
|
|
252
|
+
throw new Error("Access denied: File is outside the repository base path.");
|
|
253
|
+
}
|
|
254
|
+
const ext = path.extname(filepath).toLowerCase();
|
|
255
|
+
try {
|
|
256
|
+
const content = await fs.readFile(fullPath, "utf-8");
|
|
257
|
+
if (ext === ".md" || ext === ".markdown") {
|
|
258
|
+
const sections = content.split(/^# /m);
|
|
259
|
+
const lowPath = endpointPath.toLowerCase();
|
|
260
|
+
const lowMethod = method.toLowerCase();
|
|
261
|
+
const matches = sections.filter((s) => {
|
|
262
|
+
const lowSection = s.toLowerCase();
|
|
263
|
+
const hasMethod = lowSection.includes(`[${lowMethod}]`) ||
|
|
264
|
+
lowSection.includes(`**${lowMethod}**`) ||
|
|
265
|
+
lowSection.includes(`${lowMethod} `) ||
|
|
266
|
+
lowSection.includes(`| ${lowMethod} `);
|
|
267
|
+
return hasMethod && lowSection.includes(lowPath);
|
|
268
|
+
});
|
|
269
|
+
if (matches.length === 0) {
|
|
270
|
+
return {
|
|
271
|
+
content: [
|
|
272
|
+
{
|
|
273
|
+
type: "text",
|
|
274
|
+
text: `No matching section found for "${method} ${endpointPath}" in Markdown file.`,
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
isError: true,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
const result = matches
|
|
281
|
+
.map((m) => (m.trim() ? `# ${m.trim()}` : ""))
|
|
282
|
+
.join("\n\n---\n\n");
|
|
283
|
+
return {
|
|
284
|
+
content: [{ type: "text", text: optimizeMarkdown(result) }],
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const spec = yaml.load(content);
|
|
288
|
+
if (!spec || typeof spec !== "object") {
|
|
289
|
+
throw new Error("Invalid spec file format");
|
|
290
|
+
}
|
|
291
|
+
const paths = spec.paths || spec.endpoints;
|
|
292
|
+
if (!paths) {
|
|
293
|
+
throw new Error("Spec file does not contain 'paths' or 'endpoints' section");
|
|
294
|
+
}
|
|
295
|
+
const lowPath = endpointPath.toLowerCase();
|
|
296
|
+
const actualPathKey = Object.keys(paths).find((k) => k.toLowerCase() === lowPath);
|
|
297
|
+
if (!actualPathKey) {
|
|
298
|
+
return {
|
|
299
|
+
content: [
|
|
300
|
+
{
|
|
301
|
+
type: "text",
|
|
302
|
+
text: `Endpoint path "${endpointPath}" not found in spec.`,
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
isError: true,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
const pathObj = paths[actualPathKey];
|
|
309
|
+
const lowMethod = method.toLowerCase();
|
|
310
|
+
const actualMethodKey = Object.keys(pathObj).find((m) => m.toLowerCase() === lowMethod);
|
|
311
|
+
if (!actualMethodKey) {
|
|
312
|
+
return {
|
|
313
|
+
content: [
|
|
314
|
+
{
|
|
315
|
+
type: "text",
|
|
316
|
+
text: `Method "${method}" not found for path "${endpointPath}".`,
|
|
317
|
+
},
|
|
318
|
+
],
|
|
319
|
+
isError: true,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
const prunedMethodObj = pruneOpenApiObject(pathObj[actualMethodKey]);
|
|
323
|
+
const result = {
|
|
324
|
+
[actualPathKey]: {
|
|
325
|
+
[actualMethodKey]: prunedMethodObj,
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
return {
|
|
329
|
+
content: [{ type: "text", text: yaml.dump(result) }],
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
return {
|
|
334
|
+
content: [
|
|
335
|
+
{ type: "text", text: `Error processing spec: ${error.message}` },
|
|
336
|
+
],
|
|
337
|
+
isError: true,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
}
|
|
91
341
|
async function handleListDirectory(args) {
|
|
92
342
|
const dirPath = typeof args?.dirPath === "string" ? args.dirPath : ".";
|
|
93
343
|
const fullDirPath = path.resolve(REPO_BASE_PATH, dirPath);
|
|
@@ -96,11 +346,21 @@ async function handleListDirectory(args) {
|
|
|
96
346
|
}
|
|
97
347
|
try {
|
|
98
348
|
const files = await fs.readdir(fullDirPath, { withFileTypes: true });
|
|
99
|
-
|
|
100
|
-
.
|
|
101
|
-
.
|
|
349
|
+
let fileList = files
|
|
350
|
+
.filter((d) => !IGNORED_DIRS.includes(d.name))
|
|
351
|
+
.map((dirent) => `${dirent.isDirectory() ? "[DIR]" : "[FILE]"} ${dirent.name}`);
|
|
352
|
+
const total = fileList.length;
|
|
353
|
+
if (fileList.length > MAX_LIST_ENTRIES) {
|
|
354
|
+
fileList = fileList.slice(0, MAX_LIST_ENTRIES);
|
|
355
|
+
fileList.push(`\n... and ${total - MAX_LIST_ENTRIES} more entries.`);
|
|
356
|
+
}
|
|
102
357
|
return {
|
|
103
|
-
content: [
|
|
358
|
+
content: [
|
|
359
|
+
{
|
|
360
|
+
type: "text",
|
|
361
|
+
text: `Contents of ${dirPath}:\n${fileList.join("\n")}`,
|
|
362
|
+
},
|
|
363
|
+
],
|
|
104
364
|
};
|
|
105
365
|
}
|
|
106
366
|
catch (error) {
|
|
@@ -116,17 +376,15 @@ async function findFilesRecursive(dir, keyword, basePath) {
|
|
|
116
376
|
let results = [];
|
|
117
377
|
const list = await fs.readdir(dir, { withFileTypes: true });
|
|
118
378
|
for (const file of list) {
|
|
119
|
-
if (file.isDirectory() &&
|
|
120
|
-
(file.name === "node_modules" ||
|
|
121
|
-
file.name === ".git" ||
|
|
122
|
-
file.name === ".idea" ||
|
|
123
|
-
file.name === ".vscode")) {
|
|
379
|
+
if (file.isDirectory() && IGNORED_DIRS.includes(file.name)) {
|
|
124
380
|
continue;
|
|
125
381
|
}
|
|
126
382
|
const fullPath = path.resolve(dir, file.name);
|
|
127
383
|
if (file.isDirectory()) {
|
|
128
384
|
const subResults = await findFilesRecursive(fullPath, keyword, basePath);
|
|
129
385
|
results = results.concat(subResults);
|
|
386
|
+
if (results.length > MAX_SEARCH_RESULTS)
|
|
387
|
+
break;
|
|
130
388
|
}
|
|
131
389
|
else if (file.name.toLowerCase().includes(keyword.toLowerCase())) {
|
|
132
390
|
results.push(path.relative(basePath, fullPath));
|
|
@@ -137,10 +395,16 @@ async function findFilesRecursive(dir, keyword, basePath) {
|
|
|
137
395
|
async function handleSearchFiles(args) {
|
|
138
396
|
const keyword = typeof args?.keyword === "string" ? args.keyword : "";
|
|
139
397
|
if (!keyword) {
|
|
140
|
-
throw new Error("keyword is required
|
|
398
|
+
throw new Error("keyword is required");
|
|
141
399
|
}
|
|
142
400
|
try {
|
|
143
|
-
|
|
401
|
+
let matchedFiles = await findFilesRecursive(REPO_BASE_PATH, keyword, REPO_BASE_PATH);
|
|
402
|
+
const total = matchedFiles.length;
|
|
403
|
+
let warning = "";
|
|
404
|
+
if (matchedFiles.length > MAX_SEARCH_RESULTS) {
|
|
405
|
+
matchedFiles = matchedFiles.slice(0, MAX_SEARCH_RESULTS);
|
|
406
|
+
warning = `\n\nWarning: Showing first ${MAX_SEARCH_RESULTS} of ${total} results. Please use a more specific keyword.`;
|
|
407
|
+
}
|
|
144
408
|
if (matchedFiles.length === 0) {
|
|
145
409
|
return {
|
|
146
410
|
content: [
|
|
@@ -155,7 +419,7 @@ async function handleSearchFiles(args) {
|
|
|
155
419
|
content: [
|
|
156
420
|
{
|
|
157
421
|
type: "text",
|
|
158
|
-
text: `Found ${matchedFiles.length} matching files:\n${matchedFiles.join("\n")}`,
|
|
422
|
+
text: `Found ${matchedFiles.length} matching files:\n${matchedFiles.join("\n")}${warning}`,
|
|
159
423
|
},
|
|
160
424
|
],
|
|
161
425
|
};
|
|
@@ -174,6 +438,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
174
438
|
switch (name) {
|
|
175
439
|
case "read_api_spec":
|
|
176
440
|
return await handleReadApiSpec(args);
|
|
441
|
+
case "read_specific_endpoint":
|
|
442
|
+
return await handleReadSpecificEndpoint(args);
|
|
177
443
|
case "list_directory":
|
|
178
444
|
return await handleListDirectory(args);
|
|
179
445
|
case "search_files":
|
|
@@ -185,7 +451,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
185
451
|
async function run() {
|
|
186
452
|
const transport = new StdioServerTransport();
|
|
187
453
|
await server.connect(transport);
|
|
188
|
-
console.error(`MCP Server "
|
|
454
|
+
console.error(`MCP Server "http-client-mcp-server" is running on stdio.`);
|
|
189
455
|
console.error(`Watching repository path: ${REPO_BASE_PATH}`);
|
|
190
456
|
}
|
|
191
457
|
try {
|
package/index.ts
CHANGED
|
@@ -7,24 +7,153 @@ import {
|
|
|
7
7
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
8
8
|
import fs from "node:fs/promises";
|
|
9
9
|
import path from "node:path";
|
|
10
|
+
import yaml from "js-yaml";
|
|
11
|
+
|
|
12
|
+
const IGNORED_DIRS = [
|
|
13
|
+
"node_modules",
|
|
14
|
+
".git",
|
|
15
|
+
".idea",
|
|
16
|
+
".vscode",
|
|
17
|
+
"dist",
|
|
18
|
+
"build",
|
|
19
|
+
".next",
|
|
20
|
+
".cache",
|
|
21
|
+
];
|
|
22
|
+
const MAX_SEARCH_RESULTS = 50;
|
|
23
|
+
const MAX_LIST_ENTRIES = 100;
|
|
24
|
+
const MAX_FILE_SIZE_BYTES = 100 * 1024; // 100KB
|
|
25
|
+
|
|
26
|
+
function pruneOpenApiObject(obj: any): any {
|
|
27
|
+
if (!obj || typeof obj !== "object") return obj;
|
|
28
|
+
|
|
29
|
+
if (Array.isArray(obj)) {
|
|
30
|
+
return obj.map(pruneOpenApiObject);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const pruned: any = {};
|
|
34
|
+
const skipKeys = ["description", "summary", "example", "examples"];
|
|
35
|
+
|
|
36
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
37
|
+
if (skipKeys.includes(key) || key.startsWith("x-")) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
pruned[key] = pruneOpenApiObject(value);
|
|
41
|
+
}
|
|
42
|
+
return pruned;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Minifies JSON code blocks in Markdown.
|
|
47
|
+
*/
|
|
48
|
+
function minifyJsonInMarkdown(text: string): string {
|
|
49
|
+
return text.replace(/```json([\s\S]*?)```/g, (match, jsonStr) => {
|
|
50
|
+
try {
|
|
51
|
+
const minified = JSON.stringify(JSON.parse(jsonStr.trim()));
|
|
52
|
+
return `\`\`\`json\n${minified}\n\`\`\``;
|
|
53
|
+
} catch {
|
|
54
|
+
return match;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Flattens Markdown tables into concise bulleted lists.
|
|
61
|
+
*/
|
|
62
|
+
function flattenMarkdownTables(text: string): string {
|
|
63
|
+
const lines = text.split("\n");
|
|
64
|
+
const result: string[] = [];
|
|
65
|
+
let inTable = false;
|
|
66
|
+
let headers: string[] = [];
|
|
67
|
+
|
|
68
|
+
for (const line of lines) {
|
|
69
|
+
const isDivider = /^[\s|:-]+$/.test(line);
|
|
70
|
+
const isRow = line.trim().startsWith("|") && line.trim().endsWith("|");
|
|
71
|
+
|
|
72
|
+
if (isRow && !isDivider) {
|
|
73
|
+
const cells = line
|
|
74
|
+
.split("|")
|
|
75
|
+
.map((c) => c.trim())
|
|
76
|
+
.filter((c, i, arr) => i > 0 && i < arr.length - 1);
|
|
77
|
+
|
|
78
|
+
if (!inTable) {
|
|
79
|
+
inTable = true;
|
|
80
|
+
headers = cells;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// It's a data row
|
|
85
|
+
if (cells.length > 0) {
|
|
86
|
+
const formatted = cells
|
|
87
|
+
.map((cell, i) => {
|
|
88
|
+
const header = headers[i] || `col${i}`;
|
|
89
|
+
return cell ? `${header}: ${cell}` : "";
|
|
90
|
+
})
|
|
91
|
+
.filter(Boolean)
|
|
92
|
+
.join(", ");
|
|
93
|
+
result.push(`- ${formatted}`);
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
if (inTable && !isDivider && !isRow) {
|
|
97
|
+
inTable = false;
|
|
98
|
+
}
|
|
99
|
+
if (!isDivider) {
|
|
100
|
+
result.push(line);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return result.join("\n");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function optimizeMarkdown(text: string): string {
|
|
108
|
+
let optimized = text;
|
|
109
|
+
|
|
110
|
+
optimized = optimized.replace(/-- no response --/gi, "");
|
|
111
|
+
optimized = optimized.replace(/Content-Type: application\/json/gi, "");
|
|
112
|
+
|
|
113
|
+
optimized = flattenMarkdownTables(optimized);
|
|
114
|
+
|
|
115
|
+
optimized = minifyJsonInMarkdown(optimized);
|
|
116
|
+
|
|
117
|
+
const seenBlocks = new Set<string>();
|
|
118
|
+
optimized = optimized.replace(/```[\s\S]*?```/g, (match) => {
|
|
119
|
+
const cleaned = match.replace(/\s/g, ""); // canonicalize
|
|
120
|
+
if (seenBlocks.has(cleaned) && cleaned.length > 50) {
|
|
121
|
+
return "[Same body as above]";
|
|
122
|
+
}
|
|
123
|
+
seenBlocks.add(cleaned);
|
|
124
|
+
return match;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
optimized = optimized.replace(/\n\s*\n\s*\n+/g, "\n\n");
|
|
128
|
+
|
|
129
|
+
return optimized.trim();
|
|
130
|
+
}
|
|
10
131
|
|
|
11
132
|
const rawRepoPath = process.env.REPO_BASE_PATH || process.argv[2];
|
|
12
133
|
|
|
13
134
|
if (!rawRepoPath) {
|
|
14
135
|
const currentPath = process.cwd();
|
|
15
|
-
console.error(
|
|
16
|
-
|
|
17
|
-
|
|
136
|
+
console.error(
|
|
137
|
+
"Error: REPO_BASE_PATH environment variable or a command-line argument is required.",
|
|
138
|
+
);
|
|
139
|
+
console.error(
|
|
140
|
+
`\nHint: If you want to use the current directory, the absolute path is:\n${currentPath}`,
|
|
141
|
+
);
|
|
142
|
+
console.error(`\nUsage example:\nhttp-client-mcp-server ${currentPath}`);
|
|
18
143
|
console.error("\nTips to find your repository path:");
|
|
19
|
-
console.error(
|
|
20
|
-
|
|
144
|
+
console.error(
|
|
145
|
+
"- macOS: Right-click folder + hold 'Option' key -> Select 'Copy as Pathname'",
|
|
146
|
+
);
|
|
147
|
+
console.error(
|
|
148
|
+
"- Windows: Shift + Right-click folder -> Select 'Copy as path'",
|
|
149
|
+
);
|
|
21
150
|
process.exit(1);
|
|
22
151
|
}
|
|
23
152
|
|
|
24
153
|
const REPO_BASE_PATH = path.resolve(rawRepoPath);
|
|
25
154
|
|
|
26
155
|
const server = new Server(
|
|
27
|
-
{ name: "
|
|
156
|
+
{ name: "http-client-mcp-server", version: "1.0.0" },
|
|
28
157
|
{ capabilities: { tools: {} } },
|
|
29
158
|
);
|
|
30
159
|
|
|
@@ -34,7 +163,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
34
163
|
{
|
|
35
164
|
name: "read_api_spec",
|
|
36
165
|
description:
|
|
37
|
-
"Reads API specifications,
|
|
166
|
+
"Reads API specifications, source code, or any files. Supports line ranges for large files.",
|
|
38
167
|
inputSchema: {
|
|
39
168
|
type: "object",
|
|
40
169
|
properties: {
|
|
@@ -42,14 +171,48 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
42
171
|
type: "string",
|
|
43
172
|
description: "Relative path to the file inside the repo",
|
|
44
173
|
},
|
|
174
|
+
startLine: {
|
|
175
|
+
type: "number",
|
|
176
|
+
description: "Optional start line number (1-based)",
|
|
177
|
+
},
|
|
178
|
+
endLine: {
|
|
179
|
+
type: "number",
|
|
180
|
+
description: "Optional end line number (1-based)",
|
|
181
|
+
},
|
|
45
182
|
},
|
|
46
183
|
required: ["filepath"],
|
|
47
184
|
},
|
|
48
185
|
},
|
|
186
|
+
{
|
|
187
|
+
name: "read_specific_endpoint",
|
|
188
|
+
description:
|
|
189
|
+
"Reads a specific endpoint and method from an OpenAPI spec (YAML/JSON) or Markdown doc. Optimizes output to save tokens.",
|
|
190
|
+
inputSchema: {
|
|
191
|
+
type: "object",
|
|
192
|
+
properties: {
|
|
193
|
+
filepath: {
|
|
194
|
+
type: "string",
|
|
195
|
+
description:
|
|
196
|
+
"Relative path to the YAML/JSON/MD spec file inside the repo",
|
|
197
|
+
},
|
|
198
|
+
endpointPath: {
|
|
199
|
+
type: "string",
|
|
200
|
+
description:
|
|
201
|
+
"The API path (e.g., '/v1/account/work-information')",
|
|
202
|
+
},
|
|
203
|
+
method: {
|
|
204
|
+
type: "string",
|
|
205
|
+
description:
|
|
206
|
+
"The HTTP method (e.g., 'get', 'post', 'put', 'delete')",
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
required: ["filepath", "endpointPath", "method"],
|
|
210
|
+
},
|
|
211
|
+
},
|
|
49
212
|
{
|
|
50
213
|
name: "list_directory",
|
|
51
214
|
description:
|
|
52
|
-
"List files and folders in a specific directory
|
|
215
|
+
"List files and folders in a specific directory. Limits output to prevent context blowout.",
|
|
53
216
|
inputSchema: {
|
|
54
217
|
type: "object",
|
|
55
218
|
properties: {
|
|
@@ -65,14 +228,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
65
228
|
{
|
|
66
229
|
name: "search_files",
|
|
67
230
|
description:
|
|
68
|
-
"Search for files across the
|
|
231
|
+
"Search for files across the repo using a keyword. Limits results to prevent context blowout.",
|
|
69
232
|
inputSchema: {
|
|
70
233
|
type: "object",
|
|
71
234
|
properties: {
|
|
72
235
|
keyword: {
|
|
73
236
|
type: "string",
|
|
74
237
|
description:
|
|
75
|
-
"Keyword to search for in filenames (e.g., 'update-address'
|
|
238
|
+
"Keyword to search for in filenames (e.g., 'update-address')",
|
|
76
239
|
},
|
|
77
240
|
},
|
|
78
241
|
required: ["keyword"],
|
|
@@ -83,23 +246,43 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
83
246
|
});
|
|
84
247
|
|
|
85
248
|
async function handleReadApiSpec(args: any) {
|
|
86
|
-
const filepath
|
|
249
|
+
const { filepath, startLine, endLine } = args;
|
|
87
250
|
|
|
88
251
|
if (!filepath) {
|
|
89
|
-
throw new Error("filepath is required
|
|
252
|
+
throw new Error("filepath is required");
|
|
90
253
|
}
|
|
91
254
|
|
|
92
255
|
const fullPath = path.resolve(REPO_BASE_PATH, filepath);
|
|
93
|
-
|
|
94
256
|
if (!fullPath.startsWith(REPO_BASE_PATH)) {
|
|
95
|
-
console.error(`[Security Warning] Attempted path traversal: ${filepath}`);
|
|
96
257
|
throw new Error("Access denied: File is outside the repository base path.");
|
|
97
258
|
}
|
|
98
259
|
|
|
99
260
|
try {
|
|
261
|
+
const stats = await fs.stat(fullPath);
|
|
262
|
+
if (stats.size > MAX_FILE_SIZE_BYTES && !startLine && !endLine) {
|
|
263
|
+
return {
|
|
264
|
+
content: [
|
|
265
|
+
{
|
|
266
|
+
type: "text",
|
|
267
|
+
text: `File is too large (${Math.round(stats.size / 1024)}KB). Please use 'read_specific_endpoint' for API specs or specify 'startLine' and 'endLine' to read specific segments.`,
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
isError: true,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
100
274
|
const content = await fs.readFile(fullPath, "utf-8");
|
|
275
|
+
let resultText = content;
|
|
276
|
+
|
|
277
|
+
if (startLine || endLine) {
|
|
278
|
+
const lines = content.split("\n");
|
|
279
|
+
const start = startLine ? Math.max(0, startLine - 1) : 0;
|
|
280
|
+
const end = endLine ? Math.min(lines.length, endLine) : lines.length;
|
|
281
|
+
resultText = lines.slice(start, end).join("\n");
|
|
282
|
+
}
|
|
283
|
+
|
|
101
284
|
return {
|
|
102
|
-
content: [{ type: "text", text:
|
|
285
|
+
content: [{ type: "text", text: resultText }],
|
|
103
286
|
};
|
|
104
287
|
} catch (error: any) {
|
|
105
288
|
return {
|
|
@@ -109,6 +292,127 @@ async function handleReadApiSpec(args: any) {
|
|
|
109
292
|
}
|
|
110
293
|
}
|
|
111
294
|
|
|
295
|
+
async function handleReadSpecificEndpoint(args: any) {
|
|
296
|
+
const { filepath, endpointPath, method } = args;
|
|
297
|
+
|
|
298
|
+
if (!filepath || !endpointPath || !method) {
|
|
299
|
+
throw new Error("filepath, endpointPath, and method are required");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const fullPath = path.resolve(REPO_BASE_PATH, filepath);
|
|
303
|
+
if (!fullPath.startsWith(REPO_BASE_PATH)) {
|
|
304
|
+
throw new Error("Access denied: File is outside the repository base path.");
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const ext = path.extname(filepath).toLowerCase();
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
const content = await fs.readFile(fullPath, "utf-8");
|
|
311
|
+
|
|
312
|
+
if (ext === ".md" || ext === ".markdown") {
|
|
313
|
+
const sections = content.split(/^# /m);
|
|
314
|
+
const lowPath = endpointPath.toLowerCase();
|
|
315
|
+
const lowMethod = method.toLowerCase();
|
|
316
|
+
|
|
317
|
+
const matches = sections.filter((s) => {
|
|
318
|
+
const lowSection = s.toLowerCase();
|
|
319
|
+
const hasMethod =
|
|
320
|
+
lowSection.includes(`[${lowMethod}]`) ||
|
|
321
|
+
lowSection.includes(`**${lowMethod}**`) ||
|
|
322
|
+
lowSection.includes(`${lowMethod} `) ||
|
|
323
|
+
lowSection.includes(`| ${lowMethod} `);
|
|
324
|
+
|
|
325
|
+
return hasMethod && lowSection.includes(lowPath);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
if (matches.length === 0) {
|
|
329
|
+
return {
|
|
330
|
+
content: [
|
|
331
|
+
{
|
|
332
|
+
type: "text",
|
|
333
|
+
text: `No matching section found for "${method} ${endpointPath}" in Markdown file.`,
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
isError: true,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const result = matches
|
|
341
|
+
.map((m) => (m.trim() ? `# ${m.trim()}` : ""))
|
|
342
|
+
.join("\n\n---\n\n");
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
content: [{ type: "text", text: optimizeMarkdown(result) }],
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const spec = yaml.load(content) as any;
|
|
350
|
+
if (!spec || typeof spec !== "object") {
|
|
351
|
+
throw new Error("Invalid spec file format");
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const paths = spec.paths || spec.endpoints;
|
|
355
|
+
if (!paths) {
|
|
356
|
+
throw new Error(
|
|
357
|
+
"Spec file does not contain 'paths' or 'endpoints' section",
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const lowPath = endpointPath.toLowerCase();
|
|
362
|
+
const actualPathKey = Object.keys(paths).find(
|
|
363
|
+
(k) => k.toLowerCase() === lowPath,
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
if (!actualPathKey) {
|
|
367
|
+
return {
|
|
368
|
+
content: [
|
|
369
|
+
{
|
|
370
|
+
type: "text",
|
|
371
|
+
text: `Endpoint path "${endpointPath}" not found in spec.`,
|
|
372
|
+
},
|
|
373
|
+
],
|
|
374
|
+
isError: true,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const pathObj = paths[actualPathKey];
|
|
379
|
+
const lowMethod = method.toLowerCase();
|
|
380
|
+
const actualMethodKey = Object.keys(pathObj).find(
|
|
381
|
+
(m) => m.toLowerCase() === lowMethod,
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
if (!actualMethodKey) {
|
|
385
|
+
return {
|
|
386
|
+
content: [
|
|
387
|
+
{
|
|
388
|
+
type: "text",
|
|
389
|
+
text: `Method "${method}" not found for path "${endpointPath}".`,
|
|
390
|
+
},
|
|
391
|
+
],
|
|
392
|
+
isError: true,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const prunedMethodObj = pruneOpenApiObject(pathObj[actualMethodKey]);
|
|
397
|
+
const result = {
|
|
398
|
+
[actualPathKey]: {
|
|
399
|
+
[actualMethodKey]: prunedMethodObj,
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
content: [{ type: "text", text: yaml.dump(result) }],
|
|
405
|
+
};
|
|
406
|
+
} catch (error: any) {
|
|
407
|
+
return {
|
|
408
|
+
content: [
|
|
409
|
+
{ type: "text", text: `Error processing spec: ${error.message}` },
|
|
410
|
+
],
|
|
411
|
+
isError: true,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
112
416
|
async function handleListDirectory(args: any) {
|
|
113
417
|
const dirPath = typeof args?.dirPath === "string" ? args.dirPath : ".";
|
|
114
418
|
const fullDirPath = path.resolve(REPO_BASE_PATH, dirPath);
|
|
@@ -119,15 +423,26 @@ async function handleListDirectory(args: any) {
|
|
|
119
423
|
|
|
120
424
|
try {
|
|
121
425
|
const files = await fs.readdir(fullDirPath, { withFileTypes: true });
|
|
122
|
-
|
|
426
|
+
let fileList = files
|
|
427
|
+
.filter((d) => !IGNORED_DIRS.includes(d.name))
|
|
123
428
|
.map(
|
|
124
429
|
(dirent) =>
|
|
125
430
|
`${dirent.isDirectory() ? "[DIR]" : "[FILE]"} ${dirent.name}`,
|
|
126
|
-
)
|
|
127
|
-
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
const total = fileList.length;
|
|
434
|
+
if (fileList.length > MAX_LIST_ENTRIES) {
|
|
435
|
+
fileList = fileList.slice(0, MAX_LIST_ENTRIES);
|
|
436
|
+
fileList.push(`\n... and ${total - MAX_LIST_ENTRIES} more entries.`);
|
|
437
|
+
}
|
|
128
438
|
|
|
129
439
|
return {
|
|
130
|
-
content: [
|
|
440
|
+
content: [
|
|
441
|
+
{
|
|
442
|
+
type: "text",
|
|
443
|
+
text: `Contents of ${dirPath}:\n${fileList.join("\n")}`,
|
|
444
|
+
},
|
|
445
|
+
],
|
|
131
446
|
};
|
|
132
447
|
} catch (error: any) {
|
|
133
448
|
return {
|
|
@@ -148,13 +463,7 @@ async function findFilesRecursive(
|
|
|
148
463
|
const list = await fs.readdir(dir, { withFileTypes: true });
|
|
149
464
|
|
|
150
465
|
for (const file of list) {
|
|
151
|
-
if (
|
|
152
|
-
file.isDirectory() &&
|
|
153
|
-
(file.name === "node_modules" ||
|
|
154
|
-
file.name === ".git" ||
|
|
155
|
-
file.name === ".idea" ||
|
|
156
|
-
file.name === ".vscode")
|
|
157
|
-
) {
|
|
466
|
+
if (file.isDirectory() && IGNORED_DIRS.includes(file.name)) {
|
|
158
467
|
continue;
|
|
159
468
|
}
|
|
160
469
|
|
|
@@ -163,6 +472,7 @@ async function findFilesRecursive(
|
|
|
163
472
|
if (file.isDirectory()) {
|
|
164
473
|
const subResults = await findFilesRecursive(fullPath, keyword, basePath);
|
|
165
474
|
results = results.concat(subResults);
|
|
475
|
+
if (results.length > MAX_SEARCH_RESULTS) break;
|
|
166
476
|
} else if (file.name.toLowerCase().includes(keyword.toLowerCase())) {
|
|
167
477
|
results.push(path.relative(basePath, fullPath));
|
|
168
478
|
}
|
|
@@ -174,16 +484,23 @@ async function handleSearchFiles(args: any) {
|
|
|
174
484
|
const keyword = typeof args?.keyword === "string" ? args.keyword : "";
|
|
175
485
|
|
|
176
486
|
if (!keyword) {
|
|
177
|
-
throw new Error("keyword is required
|
|
487
|
+
throw new Error("keyword is required");
|
|
178
488
|
}
|
|
179
489
|
|
|
180
490
|
try {
|
|
181
|
-
|
|
491
|
+
let matchedFiles = await findFilesRecursive(
|
|
182
492
|
REPO_BASE_PATH,
|
|
183
493
|
keyword,
|
|
184
494
|
REPO_BASE_PATH,
|
|
185
495
|
);
|
|
186
496
|
|
|
497
|
+
const total = matchedFiles.length;
|
|
498
|
+
let warning = "";
|
|
499
|
+
if (matchedFiles.length > MAX_SEARCH_RESULTS) {
|
|
500
|
+
matchedFiles = matchedFiles.slice(0, MAX_SEARCH_RESULTS);
|
|
501
|
+
warning = `\n\nWarning: Showing first ${MAX_SEARCH_RESULTS} of ${total} results. Please use a more specific keyword.`;
|
|
502
|
+
}
|
|
503
|
+
|
|
187
504
|
if (matchedFiles.length === 0) {
|
|
188
505
|
return {
|
|
189
506
|
content: [
|
|
@@ -199,7 +516,7 @@ async function handleSearchFiles(args: any) {
|
|
|
199
516
|
content: [
|
|
200
517
|
{
|
|
201
518
|
type: "text",
|
|
202
|
-
text: `Found ${matchedFiles.length} matching files:\n${matchedFiles.join("\n")}`,
|
|
519
|
+
text: `Found ${matchedFiles.length} matching files:\n${matchedFiles.join("\n")}${warning}`,
|
|
203
520
|
},
|
|
204
521
|
],
|
|
205
522
|
};
|
|
@@ -219,6 +536,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
219
536
|
switch (name) {
|
|
220
537
|
case "read_api_spec":
|
|
221
538
|
return await handleReadApiSpec(args);
|
|
539
|
+
case "read_specific_endpoint":
|
|
540
|
+
return await handleReadSpecificEndpoint(args);
|
|
222
541
|
case "list_directory":
|
|
223
542
|
return await handleListDirectory(args);
|
|
224
543
|
case "search_files":
|
|
@@ -231,7 +550,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
231
550
|
async function run() {
|
|
232
551
|
const transport = new StdioServerTransport();
|
|
233
552
|
await server.connect(transport);
|
|
234
|
-
console.error(`MCP Server "
|
|
553
|
+
console.error(`MCP Server "http-client-mcp-server" is running on stdio.`);
|
|
235
554
|
console.error(`Watching repository path: ${REPO_BASE_PATH}`);
|
|
236
555
|
}
|
|
237
556
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "http-client-mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "MCP server to explore and read files from a repository",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -16,9 +16,11 @@
|
|
|
16
16
|
"license": "ISC",
|
|
17
17
|
"type": "module",
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
19
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
20
|
+
"js-yaml": "^4.1.1"
|
|
20
21
|
},
|
|
21
22
|
"devDependencies": {
|
|
23
|
+
"@types/js-yaml": "^4.0.9",
|
|
22
24
|
"@types/node": "^25.5.2",
|
|
23
25
|
"typescript": "^6.0.2"
|
|
24
26
|
}
|