@anyshift/mcp-proxy 0.5.0 → 0.6.1
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/dist/docs/handler.d.ts +29 -0
- package/dist/docs/handler.js +192 -0
- package/dist/docs/index.d.ts +37 -0
- package/dist/docs/index.js +49 -0
- package/dist/docs/tool.d.ts +71 -0
- package/dist/docs/tool.js +105 -0
- package/dist/docs/types.d.ts +49 -0
- package/dist/docs/types.js +1 -0
- package/dist/index.js +122 -10
- package/package.json +2 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { DocsConfig } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Execute docs_glob to find files matching a pattern
|
|
4
|
+
* Results are filtered against the indexed paths to ensure consistency with docs_read
|
|
5
|
+
*/
|
|
6
|
+
export declare function executeDocsGlob(config: DocsConfig, pattern: string): Promise<{
|
|
7
|
+
content: Array<{
|
|
8
|
+
type: 'text';
|
|
9
|
+
text: string;
|
|
10
|
+
}>;
|
|
11
|
+
}>;
|
|
12
|
+
/**
|
|
13
|
+
* Execute docs_grep to search documentation content
|
|
14
|
+
*/
|
|
15
|
+
export declare function executeDocsGrep(config: DocsConfig, query: string, maxResults?: number): Promise<{
|
|
16
|
+
content: Array<{
|
|
17
|
+
type: 'text';
|
|
18
|
+
text: string;
|
|
19
|
+
}>;
|
|
20
|
+
}>;
|
|
21
|
+
/**
|
|
22
|
+
* Execute docs_read to read a documentation file
|
|
23
|
+
*/
|
|
24
|
+
export declare function executeDocsRead(config: DocsConfig, relativePath: string): Promise<{
|
|
25
|
+
content: Array<{
|
|
26
|
+
type: 'text';
|
|
27
|
+
text: string;
|
|
28
|
+
}>;
|
|
29
|
+
}>;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { glob } from 'glob';
|
|
4
|
+
// Maximum file size to read (5MB) - prevents DoS with large files
|
|
5
|
+
const MAX_FILE_SIZE_BYTES = 5 * 1024 * 1024;
|
|
6
|
+
/**
|
|
7
|
+
* Validate that the docs base path exists and is a directory
|
|
8
|
+
*/
|
|
9
|
+
function validateDocsBasePath(docsBasePath) {
|
|
10
|
+
if (!path.isAbsolute(docsBasePath)) {
|
|
11
|
+
throw new Error(`Docs base path must be absolute: ${docsBasePath}`);
|
|
12
|
+
}
|
|
13
|
+
if (!fs.existsSync(docsBasePath)) {
|
|
14
|
+
throw new Error(`Docs base path does not exist: ${docsBasePath}`);
|
|
15
|
+
}
|
|
16
|
+
const stat = fs.statSync(docsBasePath);
|
|
17
|
+
if (!stat.isDirectory()) {
|
|
18
|
+
throw new Error(`Docs base path is not a directory: ${docsBasePath}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Validate that a path is strictly within the docs base directory (prevent path traversal).
|
|
23
|
+
* Also resolves symlinks to prevent symlink-based escapes.
|
|
24
|
+
* Returns the real (symlink-resolved) absolute path.
|
|
25
|
+
*/
|
|
26
|
+
function validatePathWithinDocs(filePath, docsBasePath) {
|
|
27
|
+
// Resolve to absolute path
|
|
28
|
+
const absolutePath = path.resolve(docsBasePath, filePath);
|
|
29
|
+
// Get the real path of the base directory (resolve any symlinks)
|
|
30
|
+
const realBase = fs.realpathSync(docsBasePath);
|
|
31
|
+
// Ensure the resolved path is strictly within the docs directory (not equal to it)
|
|
32
|
+
if (absolutePath === realBase || !absolutePath.startsWith(realBase + path.sep)) {
|
|
33
|
+
throw new Error(`Path traversal detected: ${filePath} resolves outside docs directory`);
|
|
34
|
+
}
|
|
35
|
+
// If the file exists, resolve symlinks and validate the real path
|
|
36
|
+
if (fs.existsSync(absolutePath)) {
|
|
37
|
+
const realPath = fs.realpathSync(absolutePath);
|
|
38
|
+
if (!realPath.startsWith(realBase + path.sep)) {
|
|
39
|
+
throw new Error(`Symlink escape detected: ${filePath} points outside docs directory`);
|
|
40
|
+
}
|
|
41
|
+
return realPath;
|
|
42
|
+
}
|
|
43
|
+
return absolutePath;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Validate that a relative path doesn't contain path traversal sequences.
|
|
47
|
+
* Used to validate indexed paths before use.
|
|
48
|
+
*/
|
|
49
|
+
function isValidRelativePath(relativePath) {
|
|
50
|
+
// Reject paths with traversal sequences
|
|
51
|
+
const normalized = path.normalize(relativePath);
|
|
52
|
+
return !normalized.startsWith('..') && !path.isAbsolute(normalized);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Execute docs_glob to find files matching a pattern
|
|
56
|
+
* Results are filtered against the indexed paths to ensure consistency with docs_read
|
|
57
|
+
*/
|
|
58
|
+
export async function executeDocsGlob(config, pattern) {
|
|
59
|
+
validateDocsBasePath(config.docsBasePath);
|
|
60
|
+
// Execute glob within the docs directory
|
|
61
|
+
const globMatches = await glob(pattern, {
|
|
62
|
+
cwd: config.docsBasePath,
|
|
63
|
+
nodir: true, // Only match files, not directories
|
|
64
|
+
dot: false, // Don't match hidden files
|
|
65
|
+
});
|
|
66
|
+
// Filter to only include paths that are in the index (ensures docs_read will accept them)
|
|
67
|
+
const indexedSet = new Set(config.indexedPaths);
|
|
68
|
+
const matches = globMatches.filter((p) => indexedSet.has(p));
|
|
69
|
+
// Sort matches alphabetically for consistent results
|
|
70
|
+
matches.sort();
|
|
71
|
+
const result = {
|
|
72
|
+
matchingPaths: matches,
|
|
73
|
+
totalMatches: matches.length,
|
|
74
|
+
};
|
|
75
|
+
const output = matches.length > 0
|
|
76
|
+
? `Found ${matches.length} matching file(s):\n${matches.map((p) => ` - ${p}`).join('\n')}`
|
|
77
|
+
: `No files found matching pattern: ${pattern}`;
|
|
78
|
+
return {
|
|
79
|
+
content: [{ type: 'text', text: output }],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Execute docs_grep to search documentation content
|
|
84
|
+
*/
|
|
85
|
+
export async function executeDocsGrep(config, query, maxResults = 50) {
|
|
86
|
+
validateDocsBasePath(config.docsBasePath);
|
|
87
|
+
if (!query || query.trim() === '') {
|
|
88
|
+
throw new Error('Search query cannot be empty');
|
|
89
|
+
}
|
|
90
|
+
const searchQuery = query.toLowerCase();
|
|
91
|
+
const matches = [];
|
|
92
|
+
// Search through all indexed paths
|
|
93
|
+
for (const relativePath of config.indexedPaths) {
|
|
94
|
+
// Validate indexed path doesn't contain traversal sequences
|
|
95
|
+
if (!isValidRelativePath(relativePath)) {
|
|
96
|
+
continue; // Skip potentially malicious paths
|
|
97
|
+
}
|
|
98
|
+
const absolutePath = path.join(config.docsBasePath, relativePath);
|
|
99
|
+
// Skip if file doesn't exist
|
|
100
|
+
if (!fs.existsSync(absolutePath)) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
// Check file size to prevent DoS
|
|
105
|
+
const stat = fs.statSync(absolutePath);
|
|
106
|
+
if (stat.size > MAX_FILE_SIZE_BYTES) {
|
|
107
|
+
console.warn(`[docs] Skipping large file during grep: ${relativePath} (${Math.round(stat.size / 1024)}KB exceeds ${MAX_FILE_SIZE_BYTES / 1024}KB limit)`);
|
|
108
|
+
continue; // Skip files that are too large
|
|
109
|
+
}
|
|
110
|
+
const content = fs.readFileSync(absolutePath, 'utf-8');
|
|
111
|
+
const lines = content.split('\n');
|
|
112
|
+
for (let i = 0; i < lines.length; i++) {
|
|
113
|
+
if (lines[i].toLowerCase().includes(searchQuery)) {
|
|
114
|
+
matches.push({
|
|
115
|
+
path: relativePath,
|
|
116
|
+
lineNumber: i + 1,
|
|
117
|
+
lineContent: lines[i].trim().substring(0, 200), // Truncate long lines
|
|
118
|
+
});
|
|
119
|
+
if (matches.length >= maxResults) {
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (matches.length >= maxResults) {
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Skip files that can't be read
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const result = {
|
|
134
|
+
matches,
|
|
135
|
+
totalMatches: matches.length,
|
|
136
|
+
truncated: matches.length >= maxResults,
|
|
137
|
+
};
|
|
138
|
+
let output;
|
|
139
|
+
if (matches.length === 0) {
|
|
140
|
+
output = `No matches found for query: "${query}"`;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
const lines = matches.map((m) => `${m.path}:${m.lineNumber}: ${m.lineContent}`);
|
|
144
|
+
output = `Found ${matches.length} match(es)${result.truncated ? ' (truncated)' : ''}:\n\n${lines.join('\n')}`;
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
content: [{ type: 'text', text: output }],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Execute docs_read to read a documentation file
|
|
152
|
+
*/
|
|
153
|
+
export async function executeDocsRead(config, relativePath) {
|
|
154
|
+
validateDocsBasePath(config.docsBasePath);
|
|
155
|
+
if (!relativePath || relativePath.trim() === '') {
|
|
156
|
+
throw new Error('File path cannot be empty');
|
|
157
|
+
}
|
|
158
|
+
// Normalize the path
|
|
159
|
+
const normalizedPath = path.normalize(relativePath).replace(/^\/+/, '');
|
|
160
|
+
// Validate path doesn't contain traversal sequences
|
|
161
|
+
if (!isValidRelativePath(normalizedPath)) {
|
|
162
|
+
throw new Error(`Invalid path: ${relativePath}`);
|
|
163
|
+
}
|
|
164
|
+
// Check if the path is in the indexed paths
|
|
165
|
+
if (!config.indexedPaths.includes(normalizedPath)) {
|
|
166
|
+
throw new Error(`Path not in documentation index: ${relativePath}\n\n` +
|
|
167
|
+
`Use docs_glob to find valid paths. The documentation index contains ${config.indexedPaths.length} pages.`);
|
|
168
|
+
}
|
|
169
|
+
// Validate path is within docs directory and resolve symlinks
|
|
170
|
+
// This returns the real (symlink-resolved) path
|
|
171
|
+
const realPath = validatePathWithinDocs(normalizedPath, config.docsBasePath);
|
|
172
|
+
// Check if file exists
|
|
173
|
+
if (!fs.existsSync(realPath)) {
|
|
174
|
+
throw new Error(`Documentation file not found: ${relativePath}`);
|
|
175
|
+
}
|
|
176
|
+
// Check file size to prevent DoS
|
|
177
|
+
const stat = fs.statSync(realPath);
|
|
178
|
+
if (stat.size > MAX_FILE_SIZE_BYTES) {
|
|
179
|
+
throw new Error(`File too large: ${relativePath} (${Math.round(stat.size / 1024)}KB exceeds ${MAX_FILE_SIZE_BYTES / 1024}KB limit)`);
|
|
180
|
+
}
|
|
181
|
+
// Read the file using the validated real path
|
|
182
|
+
const content = fs.readFileSync(realPath, 'utf-8');
|
|
183
|
+
const lineCount = content.split('\n').length;
|
|
184
|
+
const result = {
|
|
185
|
+
path: normalizedPath,
|
|
186
|
+
content,
|
|
187
|
+
lineCount,
|
|
188
|
+
};
|
|
189
|
+
return {
|
|
190
|
+
content: [{ type: 'text', text: content }],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { CallToolRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { DOCS_GLOB_TOOL_DEFINITION, DOCS_GREP_TOOL_DEFINITION, DOCS_READ_TOOL_DEFINITION } from './tool.js';
|
|
3
|
+
import type { DocsConfig } from './types.js';
|
|
4
|
+
export type { DocsConfig } from './types.js';
|
|
5
|
+
export { DOCS_GLOB_TOOL_DEFINITION, DOCS_GREP_TOOL_DEFINITION, DOCS_READ_TOOL_DEFINITION, };
|
|
6
|
+
/**
|
|
7
|
+
* Tool definition with handler for MCP registration
|
|
8
|
+
*/
|
|
9
|
+
export interface DocsTool {
|
|
10
|
+
toolDefinition: {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
inputSchema: object;
|
|
14
|
+
};
|
|
15
|
+
handler: (request: CallToolRequest) => Promise<{
|
|
16
|
+
content: Array<{
|
|
17
|
+
type: 'text';
|
|
18
|
+
text: string;
|
|
19
|
+
}>;
|
|
20
|
+
}>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Create the docs_glob tool
|
|
24
|
+
*/
|
|
25
|
+
export declare function createDocsGlobTool(config: DocsConfig): DocsTool;
|
|
26
|
+
/**
|
|
27
|
+
* Create the docs_grep tool
|
|
28
|
+
*/
|
|
29
|
+
export declare function createDocsGrepTool(config: DocsConfig): DocsTool;
|
|
30
|
+
/**
|
|
31
|
+
* Create the docs_read tool
|
|
32
|
+
*/
|
|
33
|
+
export declare function createDocsReadTool(config: DocsConfig): DocsTool;
|
|
34
|
+
/**
|
|
35
|
+
* Create all docs tools at once
|
|
36
|
+
*/
|
|
37
|
+
export declare function createDocsTools(config: DocsConfig): DocsTool[];
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { DocsGlobSchema, DocsGrepSchema, DocsReadSchema, DOCS_GLOB_TOOL_DEFINITION, DOCS_GREP_TOOL_DEFINITION, DOCS_READ_TOOL_DEFINITION, } from './tool.js';
|
|
2
|
+
import { executeDocsGlob, executeDocsGrep, executeDocsRead } from './handler.js';
|
|
3
|
+
export { DOCS_GLOB_TOOL_DEFINITION, DOCS_GREP_TOOL_DEFINITION, DOCS_READ_TOOL_DEFINITION, };
|
|
4
|
+
/**
|
|
5
|
+
* Create the docs_glob tool
|
|
6
|
+
*/
|
|
7
|
+
export function createDocsGlobTool(config) {
|
|
8
|
+
return {
|
|
9
|
+
toolDefinition: DOCS_GLOB_TOOL_DEFINITION,
|
|
10
|
+
handler: async (request) => {
|
|
11
|
+
const parsed = DocsGlobSchema.parse(request.params.arguments);
|
|
12
|
+
return executeDocsGlob(config, parsed.pattern);
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Create the docs_grep tool
|
|
18
|
+
*/
|
|
19
|
+
export function createDocsGrepTool(config) {
|
|
20
|
+
return {
|
|
21
|
+
toolDefinition: DOCS_GREP_TOOL_DEFINITION,
|
|
22
|
+
handler: async (request) => {
|
|
23
|
+
const parsed = DocsGrepSchema.parse(request.params.arguments);
|
|
24
|
+
return executeDocsGrep(config, parsed.query, parsed.maxResults);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Create the docs_read tool
|
|
30
|
+
*/
|
|
31
|
+
export function createDocsReadTool(config) {
|
|
32
|
+
return {
|
|
33
|
+
toolDefinition: DOCS_READ_TOOL_DEFINITION,
|
|
34
|
+
handler: async (request) => {
|
|
35
|
+
const parsed = DocsReadSchema.parse(request.params.arguments);
|
|
36
|
+
return executeDocsRead(config, parsed.path);
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Create all docs tools at once
|
|
42
|
+
*/
|
|
43
|
+
export function createDocsTools(config) {
|
|
44
|
+
return [
|
|
45
|
+
createDocsGlobTool(config),
|
|
46
|
+
createDocsGrepTool(config),
|
|
47
|
+
createDocsReadTool(config),
|
|
48
|
+
];
|
|
49
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const DocsGlobSchema: z.ZodObject<{
|
|
3
|
+
pattern: z.ZodString;
|
|
4
|
+
}, "strip", z.ZodTypeAny, {
|
|
5
|
+
pattern: string;
|
|
6
|
+
}, {
|
|
7
|
+
pattern: string;
|
|
8
|
+
}>;
|
|
9
|
+
export declare const DOCS_GLOB_TOOL_DEFINITION: {
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: string;
|
|
14
|
+
properties: {
|
|
15
|
+
pattern: {
|
|
16
|
+
type: string;
|
|
17
|
+
description: string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
required: string[];
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
export declare const DocsGrepSchema: z.ZodObject<{
|
|
24
|
+
query: z.ZodString;
|
|
25
|
+
maxResults: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
26
|
+
}, "strip", z.ZodTypeAny, {
|
|
27
|
+
query: string;
|
|
28
|
+
maxResults: number;
|
|
29
|
+
}, {
|
|
30
|
+
query: string;
|
|
31
|
+
maxResults?: number | undefined;
|
|
32
|
+
}>;
|
|
33
|
+
export declare const DOCS_GREP_TOOL_DEFINITION: {
|
|
34
|
+
name: string;
|
|
35
|
+
description: string;
|
|
36
|
+
inputSchema: {
|
|
37
|
+
type: string;
|
|
38
|
+
properties: {
|
|
39
|
+
query: {
|
|
40
|
+
type: string;
|
|
41
|
+
description: string;
|
|
42
|
+
};
|
|
43
|
+
maxResults: {
|
|
44
|
+
type: string;
|
|
45
|
+
description: string;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
required: string[];
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
export declare const DocsReadSchema: z.ZodObject<{
|
|
52
|
+
path: z.ZodString;
|
|
53
|
+
}, "strip", z.ZodTypeAny, {
|
|
54
|
+
path: string;
|
|
55
|
+
}, {
|
|
56
|
+
path: string;
|
|
57
|
+
}>;
|
|
58
|
+
export declare const DOCS_READ_TOOL_DEFINITION: {
|
|
59
|
+
name: string;
|
|
60
|
+
description: string;
|
|
61
|
+
inputSchema: {
|
|
62
|
+
type: string;
|
|
63
|
+
properties: {
|
|
64
|
+
path: {
|
|
65
|
+
type: string;
|
|
66
|
+
description: string;
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
required: string[];
|
|
70
|
+
};
|
|
71
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// docs_glob - Find documentation files matching a pattern
|
|
4
|
+
// =============================================================================
|
|
5
|
+
export const DocsGlobSchema = z.object({
|
|
6
|
+
pattern: z
|
|
7
|
+
.string()
|
|
8
|
+
.describe('Glob pattern to match documentation files. Examples: "*.mdx", "integrations/**/*.mdx", "**/datadog*"'),
|
|
9
|
+
});
|
|
10
|
+
export const DOCS_GLOB_TOOL_DEFINITION = {
|
|
11
|
+
name: 'docs_glob',
|
|
12
|
+
description: `Find documentation files matching a glob pattern.
|
|
13
|
+
|
|
14
|
+
Use this tool to discover which documentation pages exist before reading them.
|
|
15
|
+
|
|
16
|
+
**Examples:**
|
|
17
|
+
- Find all MDX files: \`pattern: "**/*.mdx"\`
|
|
18
|
+
- Find integration docs: \`pattern: "integrations/**/*.mdx"\`
|
|
19
|
+
- Find pages mentioning datadog: \`pattern: "**/datadog*"\`
|
|
20
|
+
- Find all pages in a section: \`pattern: "setup/*.mdx"\`
|
|
21
|
+
|
|
22
|
+
**Returns:** List of matching file paths (relative to docs root).`,
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
pattern: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
description: 'Glob pattern to match documentation files. Examples: "*.mdx", "integrations/**/*.mdx"',
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
required: ['pattern'],
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
// =============================================================================
|
|
35
|
+
// docs_grep - Search documentation content
|
|
36
|
+
// =============================================================================
|
|
37
|
+
export const DocsGrepSchema = z.object({
|
|
38
|
+
query: z
|
|
39
|
+
.string()
|
|
40
|
+
.describe('Search query (case-insensitive substring match). Examples: "datadog integration", "API key", "webhook"'),
|
|
41
|
+
maxResults: z
|
|
42
|
+
.number()
|
|
43
|
+
.optional()
|
|
44
|
+
.default(50)
|
|
45
|
+
.describe('Maximum number of matches to return. Default: 50'),
|
|
46
|
+
});
|
|
47
|
+
export const DOCS_GREP_TOOL_DEFINITION = {
|
|
48
|
+
name: 'docs_grep',
|
|
49
|
+
description: `Search documentation content for a query string.
|
|
50
|
+
|
|
51
|
+
Use this tool to find which documentation pages contain specific information.
|
|
52
|
+
|
|
53
|
+
**Examples:**
|
|
54
|
+
- Find pages about Datadog: \`query: "datadog"\`
|
|
55
|
+
- Find API setup instructions: \`query: "API key"\`
|
|
56
|
+
- Find webhook documentation: \`query: "webhook configuration"\`
|
|
57
|
+
|
|
58
|
+
**Returns:** List of matches with file path, line number, and line content.
|
|
59
|
+
Results are sorted by relevance (number of matches per file).`,
|
|
60
|
+
inputSchema: {
|
|
61
|
+
type: 'object',
|
|
62
|
+
properties: {
|
|
63
|
+
query: {
|
|
64
|
+
type: 'string',
|
|
65
|
+
description: 'Search query (case-insensitive substring match). Examples: "datadog", "API key"',
|
|
66
|
+
},
|
|
67
|
+
maxResults: {
|
|
68
|
+
type: 'number',
|
|
69
|
+
description: 'Maximum number of matches to return. Default: 50',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
required: ['query'],
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
// =============================================================================
|
|
76
|
+
// docs_read - Read a documentation page
|
|
77
|
+
// =============================================================================
|
|
78
|
+
export const DocsReadSchema = z.object({
|
|
79
|
+
path: z
|
|
80
|
+
.string()
|
|
81
|
+
.describe('Path to the documentation file (relative to docs root). Must be an indexed path from docs_glob results.'),
|
|
82
|
+
});
|
|
83
|
+
export const DOCS_READ_TOOL_DEFINITION = {
|
|
84
|
+
name: 'docs_read',
|
|
85
|
+
description: `Read the content of a documentation page.
|
|
86
|
+
|
|
87
|
+
**Important:** You can only read paths that exist in the documentation index.
|
|
88
|
+
Use docs_glob first to find valid paths.
|
|
89
|
+
|
|
90
|
+
**Examples:**
|
|
91
|
+
- Read a specific page: \`path: "integrations/datadog.mdx"\`
|
|
92
|
+
- Read setup guide: \`path: "setup/getting-started.mdx"\`
|
|
93
|
+
|
|
94
|
+
**Returns:** Full content of the documentation page.`,
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: 'object',
|
|
97
|
+
properties: {
|
|
98
|
+
path: {
|
|
99
|
+
type: 'string',
|
|
100
|
+
description: 'Path to the documentation file (relative to docs root). Must be an indexed path.',
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
required: ['path'],
|
|
104
|
+
},
|
|
105
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for the docs MCP tools
|
|
3
|
+
*/
|
|
4
|
+
export interface DocsConfig {
|
|
5
|
+
/**
|
|
6
|
+
* Base directory containing the documentation files.
|
|
7
|
+
* All tool operations are restricted to this directory.
|
|
8
|
+
*/
|
|
9
|
+
docsBasePath: string;
|
|
10
|
+
/**
|
|
11
|
+
* Pre-indexed list of allowed document paths (relative to docsBasePath).
|
|
12
|
+
* Only these paths can be read. Glob and grep can search the directory,
|
|
13
|
+
* but read operations are restricted to indexed paths.
|
|
14
|
+
*/
|
|
15
|
+
indexedPaths: string[];
|
|
16
|
+
/**
|
|
17
|
+
* Timeout for operations in milliseconds.
|
|
18
|
+
* Default: 30000 (30 seconds)
|
|
19
|
+
*/
|
|
20
|
+
timeoutMs?: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Result from docs_glob operation
|
|
24
|
+
*/
|
|
25
|
+
export interface DocsGlobResult {
|
|
26
|
+
matchingPaths: string[];
|
|
27
|
+
totalMatches: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Result from docs_grep operation
|
|
31
|
+
*/
|
|
32
|
+
export interface DocsGrepMatch {
|
|
33
|
+
path: string;
|
|
34
|
+
lineNumber: number;
|
|
35
|
+
lineContent: string;
|
|
36
|
+
}
|
|
37
|
+
export interface DocsGrepResult {
|
|
38
|
+
matches: DocsGrepMatch[];
|
|
39
|
+
totalMatches: number;
|
|
40
|
+
truncated: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Result from docs_read operation
|
|
44
|
+
*/
|
|
45
|
+
export interface DocsReadResult {
|
|
46
|
+
path: string;
|
|
47
|
+
content: string;
|
|
48
|
+
lineCount: number;
|
|
49
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
|
22
22
|
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
23
23
|
import { createJqTool } from './jq/index.js';
|
|
24
24
|
import { createTimeseriesTool } from './timeseries/index.js';
|
|
25
|
+
import { createDocsTools } from './docs/index.js';
|
|
25
26
|
import { truncateResponseIfNeeded } from './truncation/index.js';
|
|
26
27
|
import { createFileWriter } from './fileWriter/index.js';
|
|
27
28
|
import { generateToolId } from './utils/filename.js';
|
|
@@ -185,6 +186,27 @@ const JQ_TIMEOUT_MS = parseInt(process.env.MCP_PROXY_JQ_TIMEOUT_MS || '30000');
|
|
|
185
186
|
* This tool allows detecting anomalies in time series data extracted from JSON files
|
|
186
187
|
*/
|
|
187
188
|
const ENABLE_TIMESERIES = process.env.MCP_PROXY_ENABLE_TIMESERIES !== 'false'; // default true
|
|
189
|
+
/**
|
|
190
|
+
* MCP_PROXY_ENABLE_DOCS (OPTIONAL, default: false)
|
|
191
|
+
* Enable the documentation tools (docs_glob, docs_grep, docs_read)
|
|
192
|
+
* Requires DOCS_BASE_PATH and DOCS_INDEXED_PATHS to be configured
|
|
193
|
+
*/
|
|
194
|
+
const ENABLE_DOCS = process.env.MCP_PROXY_ENABLE_DOCS === 'true'; // default false
|
|
195
|
+
/**
|
|
196
|
+
* MCP_PROXY_DOCS_BASE_PATH (REQUIRED if ENABLE_DOCS=true)
|
|
197
|
+
* Base directory containing the documentation files
|
|
198
|
+
* All doc tool operations are restricted to this directory
|
|
199
|
+
*/
|
|
200
|
+
const DOCS_BASE_PATH = process.env.MCP_PROXY_DOCS_BASE_PATH || '';
|
|
201
|
+
/**
|
|
202
|
+
* MCP_PROXY_DOCS_INDEXED_PATHS (REQUIRED if ENABLE_DOCS=true)
|
|
203
|
+
* Comma-separated list of indexed document paths (relative to DOCS_BASE_PATH)
|
|
204
|
+
* Only these paths can be read via docs_read
|
|
205
|
+
* Example: "integrations/datadog.mdx,setup/getting-started.mdx,faq.mdx"
|
|
206
|
+
*/
|
|
207
|
+
const DOCS_INDEXED_PATHS = process.env.MCP_PROXY_DOCS_INDEXED_PATHS
|
|
208
|
+
? process.env.MCP_PROXY_DOCS_INDEXED_PATHS.split(',').map(s => s.trim()).filter(Boolean)
|
|
209
|
+
: [];
|
|
188
210
|
/**
|
|
189
211
|
* MCP_PROXY_SCHEMA_MAX_DEPTH (OPTIONAL, default: 3)
|
|
190
212
|
* Maximum depth to traverse when generating query-assist schemas
|
|
@@ -230,6 +252,16 @@ if (WRITE_TO_FILE && !OUTPUT_PATH) {
|
|
|
230
252
|
console.error('Example: export MCP_PROXY_OUTPUT_PATH="/tmp/mcp-results"');
|
|
231
253
|
process.exit(1);
|
|
232
254
|
}
|
|
255
|
+
if (ENABLE_DOCS && !DOCS_BASE_PATH) {
|
|
256
|
+
console.error('ERROR: MCP_PROXY_DOCS_BASE_PATH is required when MCP_PROXY_ENABLE_DOCS=true');
|
|
257
|
+
console.error('Example: export MCP_PROXY_DOCS_BASE_PATH="/app/docs"');
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
if (ENABLE_DOCS && DOCS_INDEXED_PATHS.length === 0) {
|
|
261
|
+
console.error('ERROR: MCP_PROXY_DOCS_INDEXED_PATHS is required when MCP_PROXY_ENABLE_DOCS=true');
|
|
262
|
+
console.error('Example: export MCP_PROXY_DOCS_INDEXED_PATHS="page1.mdx,page2.mdx,folder/page3.mdx"');
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
233
265
|
// ============================================================================
|
|
234
266
|
// PASS-THROUGH ENVIRONMENT VARIABLES
|
|
235
267
|
// ============================================================================
|
|
@@ -282,6 +314,11 @@ if (ENABLE_LOGGING) {
|
|
|
282
314
|
}
|
|
283
315
|
console.debug(` JQ tool enabled: ${ENABLE_JQ}`);
|
|
284
316
|
console.debug(` Timeseries tool enabled: ${ENABLE_TIMESERIES}`);
|
|
317
|
+
console.debug(` Docs tools enabled: ${ENABLE_DOCS}`);
|
|
318
|
+
if (ENABLE_DOCS) {
|
|
319
|
+
console.debug(` Docs base path: ${DOCS_BASE_PATH}`);
|
|
320
|
+
console.debug(` Docs indexed paths: ${DOCS_INDEXED_PATHS.length} pages`);
|
|
321
|
+
}
|
|
285
322
|
if (CHILD_COMMAND) {
|
|
286
323
|
console.debug(` Pass-through env vars: ${Object.keys(childEnv).length}`);
|
|
287
324
|
}
|
|
@@ -326,14 +363,13 @@ async function main() {
|
|
|
326
363
|
const childTransport = new SSEClientTransport(new URL(REMOTE_URL), {
|
|
327
364
|
eventSourceInit: {
|
|
328
365
|
fetch: (url, init) => {
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
});
|
|
366
|
+
// Merge headers using the Headers API (case-insensitive set avoids duplicates
|
|
367
|
+
// when requestInit headers are also passed through init by the SDK)
|
|
368
|
+
const merged = new Headers(init?.headers);
|
|
369
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
370
|
+
merged.set(key, value);
|
|
371
|
+
}
|
|
372
|
+
return fetch(url, { ...init, headers: merged });
|
|
337
373
|
},
|
|
338
374
|
},
|
|
339
375
|
requestInit: { headers },
|
|
@@ -428,6 +464,18 @@ async function main() {
|
|
|
428
464
|
timeoutMs: JQ_TIMEOUT_MS // Uses same timeout as JQ since it runs jq internally
|
|
429
465
|
});
|
|
430
466
|
}
|
|
467
|
+
// Docs tools configuration
|
|
468
|
+
let docsTools = [];
|
|
469
|
+
if (ENABLE_DOCS) {
|
|
470
|
+
docsTools = createDocsTools({
|
|
471
|
+
docsBasePath: DOCS_BASE_PATH,
|
|
472
|
+
indexedPaths: DOCS_INDEXED_PATHS,
|
|
473
|
+
timeoutMs: JQ_TIMEOUT_MS
|
|
474
|
+
});
|
|
475
|
+
if (ENABLE_LOGGING) {
|
|
476
|
+
console.debug(`[mcp-proxy] Docs tools created: ${docsTools.map(t => t.toolDefinition.name).join(', ')}`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
431
479
|
// ------------------------------------------------------------------------
|
|
432
480
|
// 5. REGISTER ALL TOOLS (CHILD + PROXY) WITH DESCRIPTION INJECTION
|
|
433
481
|
// ------------------------------------------------------------------------
|
|
@@ -436,9 +484,10 @@ async function main() {
|
|
|
436
484
|
const allTools = [
|
|
437
485
|
...enhancedChildTools,
|
|
438
486
|
...(jqTool ? [jqTool.toolDefinition] : []), // JQ tool already has description param
|
|
439
|
-
...(timeseriesTool ? [timeseriesTool.toolDefinition] : []) // Timeseries tool already has description param
|
|
487
|
+
...(timeseriesTool ? [timeseriesTool.toolDefinition] : []), // Timeseries tool already has description param
|
|
488
|
+
...docsTools.map(t => t.toolDefinition) // Docs tools (if enabled)
|
|
440
489
|
];
|
|
441
|
-
const proxyToolCount = (jqTool ? 1 : 0) + (timeseriesTool ? 1 : 0);
|
|
490
|
+
const proxyToolCount = (jqTool ? 1 : 0) + (timeseriesTool ? 1 : 0) + docsTools.length;
|
|
442
491
|
console.debug(`[mcp-proxy] Exposing ${allTools.length} tools total (${childToolsResponse.tools.length} from child + ${proxyToolCount} proxy tools)`);
|
|
443
492
|
// ------------------------------------------------------------------------
|
|
444
493
|
// 6. HANDLE TOOL LIST REQUESTS
|
|
@@ -515,6 +564,69 @@ async function main() {
|
|
|
515
564
|
isError: result.isError
|
|
516
565
|
};
|
|
517
566
|
}
|
|
567
|
+
// Handle Docs tools locally (if enabled)
|
|
568
|
+
// Route through file writer for large doc handling (same as child MCP tools)
|
|
569
|
+
const docsToolNames = ['docs_glob', 'docs_grep', 'docs_read'];
|
|
570
|
+
if (docsToolNames.includes(toolName) && docsTools.length > 0) {
|
|
571
|
+
const docTool = docsTools.find(t => t.toolDefinition.name === toolName);
|
|
572
|
+
if (docTool) {
|
|
573
|
+
if (ENABLE_LOGGING) {
|
|
574
|
+
console.debug(`[mcp-proxy] Executing ${toolName} locally`);
|
|
575
|
+
}
|
|
576
|
+
try {
|
|
577
|
+
result = await docTool.handler({
|
|
578
|
+
params: { arguments: toolArgs }
|
|
579
|
+
});
|
|
580
|
+
const contentStr = result.content?.[0]?.text || '';
|
|
581
|
+
const originalLength = contentStr.length;
|
|
582
|
+
// Route through file writer (same pipeline as child MCP tools)
|
|
583
|
+
const unifiedResponse = await fileWriter.handleResponse(toolName, toolArgs, {
|
|
584
|
+
content: [{ type: 'text', text: contentStr }]
|
|
585
|
+
});
|
|
586
|
+
if (ENABLE_LOGGING) {
|
|
587
|
+
if (unifiedResponse.wroteToFile) {
|
|
588
|
+
console.debug(`[mcp-proxy] File written for ${toolName} (${originalLength} chars) → ${unifiedResponse.filePath}`);
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
console.debug(`[mcp-proxy] Response for ${toolName} (${originalLength} chars) returned directly`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
// If not written to file, apply truncation to outputContent
|
|
595
|
+
if (!unifiedResponse.wroteToFile && unifiedResponse.outputContent) {
|
|
596
|
+
const outputStr = typeof unifiedResponse.outputContent === 'string'
|
|
597
|
+
? unifiedResponse.outputContent
|
|
598
|
+
: JSON.stringify(unifiedResponse.outputContent);
|
|
599
|
+
const truncated = truncateResponseIfNeeded(truncationConfig, outputStr);
|
|
600
|
+
if (truncated.length < outputStr.length) {
|
|
601
|
+
if (ENABLE_LOGGING) {
|
|
602
|
+
console.debug(`[mcp-proxy] Truncated response: ${outputStr.length} → ${truncated.length} chars`);
|
|
603
|
+
}
|
|
604
|
+
// Keep as string for docs (markdown content)
|
|
605
|
+
unifiedResponse.outputContent = truncated;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
// Add retry metadata and return unified response
|
|
609
|
+
addRetryMetadata(unifiedResponse, toolArgs);
|
|
610
|
+
return {
|
|
611
|
+
content: [{
|
|
612
|
+
type: 'text',
|
|
613
|
+
text: JSON.stringify(unifiedResponse, null, 2)
|
|
614
|
+
}],
|
|
615
|
+
isError: false
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
catch (error) {
|
|
619
|
+
const tool_id = generateToolId(toolName, toolArgs, fileWriterConfig.toolAbbreviations);
|
|
620
|
+
return {
|
|
621
|
+
content: [{
|
|
622
|
+
type: 'text',
|
|
623
|
+
text: JSON.stringify(createErrorResponse(tool_id, error.message || String(error), toolArgs), null, 2)
|
|
624
|
+
}],
|
|
625
|
+
isError: true
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
518
630
|
// Forward all other tools to child MCP (if child exists)
|
|
519
631
|
if (!childClient) {
|
|
520
632
|
const tool_id = generateToolId(toolName, toolArgs, fileWriterConfig.toolAbbreviations);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anyshift/mcp-proxy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Generic MCP proxy that adds truncation, file writing, and JQ capabilities to any MCP server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
],
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@modelcontextprotocol/sdk": "^1.24.0",
|
|
17
|
+
"glob": "^13.0.0",
|
|
17
18
|
"zod": "^3.24.2"
|
|
18
19
|
},
|
|
19
20
|
"devDependencies": {
|