@bedolla/enriweb 0.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/LICENSE +619 -0
- package/README.md +182 -0
- package/dist/client/EnriProxyClient.d.ts +271 -0
- package/dist/client/EnriProxyClient.d.ts.map +1 -0
- package/dist/client/EnriProxyClient.js +221 -0
- package/dist/client/EnriProxyClient.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +148 -0
- package/dist/index.js.map +1 -0
- package/dist/package-info.d.ts +8 -0
- package/dist/package-info.d.ts.map +1 -0
- package/dist/package-info.js +28 -0
- package/dist/package-info.js.map +1 -0
- package/dist/server/EnriWebServer.d.ts +70 -0
- package/dist/server/EnriWebServer.d.ts.map +1 -0
- package/dist/server/EnriWebServer.js +218 -0
- package/dist/server/EnriWebServer.js.map +1 -0
- package/dist/shared/validation.d.ts +54 -0
- package/dist/shared/validation.d.ts.map +1 -0
- package/dist/shared/validation.js +112 -0
- package/dist/shared/validation.js.map +1 -0
- package/dist/tools/WebFetchTool.d.ts +232 -0
- package/dist/tools/WebFetchTool.d.ts.map +1 -0
- package/dist/tools/WebFetchTool.js +429 -0
- package/dist/tools/WebFetchTool.js.map +1 -0
- package/dist/tools/WebSearchRegistryVerifier.d.ts +323 -0
- package/dist/tools/WebSearchRegistryVerifier.d.ts.map +1 -0
- package/dist/tools/WebSearchRegistryVerifier.js +890 -0
- package/dist/tools/WebSearchRegistryVerifier.js.map +1 -0
- package/dist/tools/WebSearchTool.d.ts +132 -0
- package/dist/tools/WebSearchTool.d.ts.map +1 -0
- package/dist/tools/WebSearchTool.js +101 -0
- package/dist/tools/WebSearchTool.js.map +1 -0
- package/package.json +54 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.js","sourceRoot":"","sources":["../../src/shared/validation.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WEB FETCH TOOL
|
|
3
|
+
*
|
|
4
|
+
* Implements the `web_fetch` MCP tool by delegating to EnriProxy.
|
|
5
|
+
*
|
|
6
|
+
* @module tools/WebFetchTool
|
|
7
|
+
*/
|
|
8
|
+
import type { EnriProxyClient } from "../client/EnriProxyClient.js";
|
|
9
|
+
/**
|
|
10
|
+
* Tool parameters for `web_fetch`.
|
|
11
|
+
*/
|
|
12
|
+
export interface WebFetchToolParams {
|
|
13
|
+
/**
|
|
14
|
+
* URL to fetch.
|
|
15
|
+
*/
|
|
16
|
+
readonly url?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Cursor identifier returned by a previous call.
|
|
19
|
+
*/
|
|
20
|
+
readonly cursor?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Optional prompt for extraction.
|
|
23
|
+
*/
|
|
24
|
+
readonly prompt?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Maximum content length in characters.
|
|
27
|
+
*/
|
|
28
|
+
readonly maxChars?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Offset in characters for cursor pagination.
|
|
31
|
+
*/
|
|
32
|
+
readonly offsetChars?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Limit in characters for cursor pagination.
|
|
35
|
+
*/
|
|
36
|
+
readonly limitChars?: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Tool result for `web_fetch`.
|
|
40
|
+
*/
|
|
41
|
+
export interface WebFetchToolResult extends Record<string, unknown> {
|
|
42
|
+
/**
|
|
43
|
+
* Fetched content.
|
|
44
|
+
*/
|
|
45
|
+
readonly content: string;
|
|
46
|
+
/**
|
|
47
|
+
* HTTP status code.
|
|
48
|
+
*/
|
|
49
|
+
readonly status: number;
|
|
50
|
+
/**
|
|
51
|
+
* Content type of the response.
|
|
52
|
+
*/
|
|
53
|
+
readonly content_type: string;
|
|
54
|
+
/**
|
|
55
|
+
* Whether content was truncated.
|
|
56
|
+
*/
|
|
57
|
+
readonly truncated: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* URL that was fetched.
|
|
60
|
+
*/
|
|
61
|
+
readonly url: string;
|
|
62
|
+
/**
|
|
63
|
+
* Cursor identifier for pagination (when available).
|
|
64
|
+
*/
|
|
65
|
+
readonly cursor?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Offset in characters (cursor reads).
|
|
68
|
+
*/
|
|
69
|
+
readonly offset_chars?: number;
|
|
70
|
+
/**
|
|
71
|
+
* Limit in characters (cursor reads).
|
|
72
|
+
*/
|
|
73
|
+
readonly limit_chars?: number;
|
|
74
|
+
/**
|
|
75
|
+
* Total captured characters (cursor reads).
|
|
76
|
+
*/
|
|
77
|
+
readonly total_chars?: number;
|
|
78
|
+
/**
|
|
79
|
+
* Whether more content exists beyond this slice.
|
|
80
|
+
*/
|
|
81
|
+
readonly has_more?: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Whether content was reduced into an excerpt pack.
|
|
84
|
+
*/
|
|
85
|
+
readonly reduced?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Whether the upstream fetch was truncated.
|
|
88
|
+
*/
|
|
89
|
+
readonly fetched_truncated?: boolean;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Dependencies for {@link WebFetchTool}.
|
|
93
|
+
*/
|
|
94
|
+
export interface WebFetchToolDeps {
|
|
95
|
+
/**
|
|
96
|
+
* Creates an EnriProxy client with a base URL, API key, and timeout.
|
|
97
|
+
*
|
|
98
|
+
* @param serverUrl - EnriProxy URL
|
|
99
|
+
* @param apiKey - EnriProxy API key
|
|
100
|
+
* @param timeoutMs - Timeout in ms
|
|
101
|
+
* @returns Client instance
|
|
102
|
+
*/
|
|
103
|
+
readonly createClient: (serverUrl: string, apiKey: string, timeoutMs: number) => EnriProxyClient;
|
|
104
|
+
/**
|
|
105
|
+
* Default EnriProxy server URL.
|
|
106
|
+
*/
|
|
107
|
+
readonly defaultServerUrl: string;
|
|
108
|
+
/**
|
|
109
|
+
* Default EnriProxy API key.
|
|
110
|
+
*/
|
|
111
|
+
readonly defaultApiKey: string;
|
|
112
|
+
/**
|
|
113
|
+
* Default timeout in milliseconds.
|
|
114
|
+
*/
|
|
115
|
+
readonly defaultTimeoutMs: number;
|
|
116
|
+
/**
|
|
117
|
+
* Default maximum content length in characters returned by the tool when
|
|
118
|
+
* `max_chars` is not provided.
|
|
119
|
+
*/
|
|
120
|
+
readonly defaultMaxChars: number;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* MCP tool that fetches URL content via EnriProxy.
|
|
124
|
+
*/
|
|
125
|
+
export declare class WebFetchTool {
|
|
126
|
+
/**
|
|
127
|
+
* Readme file candidates commonly used in GitHub repositories.
|
|
128
|
+
*/
|
|
129
|
+
private static readonly README_FILENAMES;
|
|
130
|
+
/**
|
|
131
|
+
* Default branches to try when resolving GitHub raw README URLs.
|
|
132
|
+
*/
|
|
133
|
+
private static readonly README_BRANCHES;
|
|
134
|
+
/**
|
|
135
|
+
* Tool dependencies.
|
|
136
|
+
*/
|
|
137
|
+
private readonly deps;
|
|
138
|
+
/**
|
|
139
|
+
* Creates a new {@link WebFetchTool}.
|
|
140
|
+
*
|
|
141
|
+
* @param deps - Tool dependencies
|
|
142
|
+
*/
|
|
143
|
+
constructor(deps: WebFetchToolDeps);
|
|
144
|
+
/**
|
|
145
|
+
* Gets the configured default max chars for web fetch results.
|
|
146
|
+
*
|
|
147
|
+
* @returns Default max chars
|
|
148
|
+
*/
|
|
149
|
+
getDefaultMaxChars(): number;
|
|
150
|
+
/**
|
|
151
|
+
* Validates raw MCP tool arguments.
|
|
152
|
+
*
|
|
153
|
+
* @param raw - Raw tool arguments
|
|
154
|
+
* @returns Validated parameters
|
|
155
|
+
*/
|
|
156
|
+
parseParams(raw: unknown): WebFetchToolParams;
|
|
157
|
+
/**
|
|
158
|
+
* Executes the web fetch tool.
|
|
159
|
+
*
|
|
160
|
+
* @param params - Validated parameters
|
|
161
|
+
* @returns Tool result
|
|
162
|
+
*/
|
|
163
|
+
execute(params: WebFetchToolParams): Promise<WebFetchToolResult>;
|
|
164
|
+
/**
|
|
165
|
+
* Attempts to provide a higher-quality fetch for npm package pages.
|
|
166
|
+
*
|
|
167
|
+
* @param params - Tool parameters
|
|
168
|
+
* @param client - EnriProxy client
|
|
169
|
+
* @param maxChars - Maximum content length to return
|
|
170
|
+
* @returns Tool result if the URL is an npm package page, otherwise null
|
|
171
|
+
*/
|
|
172
|
+
private tryExecuteNpmPackageFetch;
|
|
173
|
+
/**
|
|
174
|
+
* Attempts to parse an npm package name from an npmjs.com package page URL.
|
|
175
|
+
*
|
|
176
|
+
* @param url - Parsed URL
|
|
177
|
+
* @returns npm package name (e.g. "chalk" or "@scope/name") or null
|
|
178
|
+
*/
|
|
179
|
+
private tryParseNpmPackageName;
|
|
180
|
+
/**
|
|
181
|
+
* Tries to parse a JSON object from a string.
|
|
182
|
+
*
|
|
183
|
+
* @param input - JSON string
|
|
184
|
+
* @returns Parsed object or null
|
|
185
|
+
*/
|
|
186
|
+
private tryParseJsonObject;
|
|
187
|
+
/**
|
|
188
|
+
* Extracts a string from an unknown value if possible.
|
|
189
|
+
*
|
|
190
|
+
* @param value - Unknown input
|
|
191
|
+
* @returns Trimmed string or null
|
|
192
|
+
*/
|
|
193
|
+
private tryGetString;
|
|
194
|
+
/**
|
|
195
|
+
* Extracts a repository URL from npm metadata.
|
|
196
|
+
*
|
|
197
|
+
* @param repository - Repository field value
|
|
198
|
+
* @returns Normalized URL string or null
|
|
199
|
+
*/
|
|
200
|
+
private tryGetRepositoryUrl;
|
|
201
|
+
/**
|
|
202
|
+
* Normalizes common git repository URL schemes into an https URL.
|
|
203
|
+
*
|
|
204
|
+
* @param rawUrl - Raw repository URL from metadata
|
|
205
|
+
* @returns Normalized URL string or null
|
|
206
|
+
*/
|
|
207
|
+
private normalizeRepositoryUrl;
|
|
208
|
+
/**
|
|
209
|
+
* Normalizes a GitHub repository URL to the canonical https form.
|
|
210
|
+
*
|
|
211
|
+
* @param repositoryUrl - Repository URL
|
|
212
|
+
* @returns Canonical GitHub repo URL (https://github.com/{owner}/{repo}) or null
|
|
213
|
+
*/
|
|
214
|
+
private tryNormalizeGitHubRepoUrl;
|
|
215
|
+
/**
|
|
216
|
+
* Attempts to fetch a GitHub repository README via raw.githubusercontent.com.
|
|
217
|
+
*
|
|
218
|
+
* @param client - EnriProxy client
|
|
219
|
+
* @param githubRepoUrl - Canonical GitHub repo URL
|
|
220
|
+
* @param maxChars - Maximum content length
|
|
221
|
+
* @returns README content if found, otherwise null
|
|
222
|
+
*/
|
|
223
|
+
private tryFetchGitHubReadme;
|
|
224
|
+
/**
|
|
225
|
+
* Formats results for MCP text output.
|
|
226
|
+
*
|
|
227
|
+
* @param result - Tool result
|
|
228
|
+
* @returns Formatted text
|
|
229
|
+
*/
|
|
230
|
+
formatOutput(result: WebFetchToolResult): string;
|
|
231
|
+
}
|
|
232
|
+
//# sourceMappingURL=WebFetchTool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"WebFetchTool.d.ts","sourceRoot":"","sources":["../../src/tools/WebFetchTool.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { assertHttpUrl, assertNonEmptyString, assertObject, optionalInt, optionalString } from "../shared/validation.js";
|
|
2
|
+
/**
|
|
3
|
+
* Default number of characters to include in the human-readable MCP output.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* The full fetched payload is still available in `structuredContent.content`,
|
|
7
|
+
* but MCP clients may enforce tool-result token limits. Keeping the human
|
|
8
|
+
* output short avoids duplication and reduces the chance of overflows.
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_TEXT_PREVIEW_CHARS = 2000;
|
|
11
|
+
/**
|
|
12
|
+
* MCP tool that fetches URL content via EnriProxy.
|
|
13
|
+
*/
|
|
14
|
+
export class WebFetchTool {
|
|
15
|
+
/**
|
|
16
|
+
* Readme file candidates commonly used in GitHub repositories.
|
|
17
|
+
*/
|
|
18
|
+
static README_FILENAMES = [
|
|
19
|
+
"README.md",
|
|
20
|
+
"readme.md"
|
|
21
|
+
];
|
|
22
|
+
/**
|
|
23
|
+
* Default branches to try when resolving GitHub raw README URLs.
|
|
24
|
+
*/
|
|
25
|
+
static README_BRANCHES = ["main", "master"];
|
|
26
|
+
/**
|
|
27
|
+
* Tool dependencies.
|
|
28
|
+
*/
|
|
29
|
+
deps;
|
|
30
|
+
/**
|
|
31
|
+
* Creates a new {@link WebFetchTool}.
|
|
32
|
+
*
|
|
33
|
+
* @param deps - Tool dependencies
|
|
34
|
+
*/
|
|
35
|
+
constructor(deps) {
|
|
36
|
+
this.deps = deps;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Gets the configured default max chars for web fetch results.
|
|
40
|
+
*
|
|
41
|
+
* @returns Default max chars
|
|
42
|
+
*/
|
|
43
|
+
getDefaultMaxChars() {
|
|
44
|
+
return this.deps.defaultMaxChars;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Validates raw MCP tool arguments.
|
|
48
|
+
*
|
|
49
|
+
* @param raw - Raw tool arguments
|
|
50
|
+
* @returns Validated parameters
|
|
51
|
+
*/
|
|
52
|
+
parseParams(raw) {
|
|
53
|
+
const obj = assertObject(raw, "arguments");
|
|
54
|
+
const cursorRaw = optionalString(obj["cursor"]);
|
|
55
|
+
const cursor = cursorRaw?.trim() ? cursorRaw.trim() : undefined;
|
|
56
|
+
const urlRaw = optionalString(obj["url"]);
|
|
57
|
+
const url = urlRaw?.trim() ? assertHttpUrl(urlRaw.trim(), "url") : undefined;
|
|
58
|
+
if (!cursor && !url) {
|
|
59
|
+
throw new Error("web_fetch requires either 'url' or 'cursor'.");
|
|
60
|
+
}
|
|
61
|
+
const prompt = optionalString(obj["prompt"]);
|
|
62
|
+
const maxChars = optionalInt(obj["max_chars"]);
|
|
63
|
+
const offsetCharsRaw = optionalInt(obj["offset_chars"]) ?? optionalInt(obj["offset"]);
|
|
64
|
+
const limitCharsRaw = optionalInt(obj["limit_chars"]) ?? optionalInt(obj["limit"]);
|
|
65
|
+
if (maxChars !== undefined && maxChars < 1) {
|
|
66
|
+
throw new Error("max_chars must be positive.");
|
|
67
|
+
}
|
|
68
|
+
const offsetChars = cursor ? offsetCharsRaw : undefined;
|
|
69
|
+
let limitChars = cursor ? limitCharsRaw : undefined;
|
|
70
|
+
if (offsetChars !== undefined && offsetChars < 0) {
|
|
71
|
+
throw new Error("offset must be non-negative.");
|
|
72
|
+
}
|
|
73
|
+
if (limitChars !== undefined) {
|
|
74
|
+
if (limitChars < 0) {
|
|
75
|
+
throw new Error("limit must be positive.");
|
|
76
|
+
}
|
|
77
|
+
if (limitChars === 0) {
|
|
78
|
+
limitChars = undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
url,
|
|
83
|
+
cursor,
|
|
84
|
+
prompt,
|
|
85
|
+
maxChars,
|
|
86
|
+
offsetChars,
|
|
87
|
+
limitChars
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Executes the web fetch tool.
|
|
92
|
+
*
|
|
93
|
+
* @param params - Validated parameters
|
|
94
|
+
* @returns Tool result
|
|
95
|
+
*/
|
|
96
|
+
async execute(params) {
|
|
97
|
+
const serverUrl = assertHttpUrl(this.deps.defaultServerUrl, "ENRIPROXY_URL");
|
|
98
|
+
const apiKey = assertNonEmptyString(this.deps.defaultApiKey, "ENRIPROXY_API_KEY");
|
|
99
|
+
const client = this.deps.createClient(serverUrl, apiKey, this.deps.defaultTimeoutMs);
|
|
100
|
+
const effectiveMaxChars = typeof params.maxChars === "number" ? params.maxChars : this.deps.defaultMaxChars;
|
|
101
|
+
if (typeof params.cursor === "string" && params.cursor.trim()) {
|
|
102
|
+
const response = await client.webFetch({
|
|
103
|
+
cursor: params.cursor.trim(),
|
|
104
|
+
offsetChars: params.offsetChars,
|
|
105
|
+
limitChars: params.limitChars,
|
|
106
|
+
maxChars: effectiveMaxChars
|
|
107
|
+
});
|
|
108
|
+
const resolvedUrl = response.url ?? params.url ?? "(cursor)";
|
|
109
|
+
return {
|
|
110
|
+
content: response.content,
|
|
111
|
+
status: response.status,
|
|
112
|
+
content_type: response.content_type,
|
|
113
|
+
truncated: response.truncated,
|
|
114
|
+
url: resolvedUrl,
|
|
115
|
+
cursor: response.cursor,
|
|
116
|
+
offset_chars: response.offset_chars,
|
|
117
|
+
limit_chars: response.limit_chars,
|
|
118
|
+
total_chars: response.total_chars,
|
|
119
|
+
has_more: response.has_more,
|
|
120
|
+
reduced: response.reduced,
|
|
121
|
+
fetched_truncated: response.fetched_truncated
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (!params.url) {
|
|
125
|
+
throw new Error("web_fetch requires a URL when cursor is not provided.");
|
|
126
|
+
}
|
|
127
|
+
const url = params.url;
|
|
128
|
+
const urlParams = {
|
|
129
|
+
...params,
|
|
130
|
+
url
|
|
131
|
+
};
|
|
132
|
+
const npmResult = await this.tryExecuteNpmPackageFetch(urlParams, client, effectiveMaxChars);
|
|
133
|
+
if (npmResult) {
|
|
134
|
+
return npmResult;
|
|
135
|
+
}
|
|
136
|
+
const response = await client.webFetch({
|
|
137
|
+
url,
|
|
138
|
+
prompt: params.prompt,
|
|
139
|
+
maxChars: effectiveMaxChars
|
|
140
|
+
});
|
|
141
|
+
return {
|
|
142
|
+
content: response.content,
|
|
143
|
+
status: response.status,
|
|
144
|
+
content_type: response.content_type,
|
|
145
|
+
truncated: response.truncated,
|
|
146
|
+
url: response.url ?? url,
|
|
147
|
+
cursor: response.cursor,
|
|
148
|
+
total_chars: response.total_chars,
|
|
149
|
+
has_more: response.has_more,
|
|
150
|
+
reduced: response.reduced,
|
|
151
|
+
fetched_truncated: response.fetched_truncated
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Attempts to provide a higher-quality fetch for npm package pages.
|
|
156
|
+
*
|
|
157
|
+
* @param params - Tool parameters
|
|
158
|
+
* @param client - EnriProxy client
|
|
159
|
+
* @param maxChars - Maximum content length to return
|
|
160
|
+
* @returns Tool result if the URL is an npm package page, otherwise null
|
|
161
|
+
*/
|
|
162
|
+
async tryExecuteNpmPackageFetch(params, client, maxChars) {
|
|
163
|
+
const requestedUrl = new URL(params.url);
|
|
164
|
+
const packageName = this.tryParseNpmPackageName(requestedUrl);
|
|
165
|
+
if (!packageName) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
const metadataUrl = `https://registry.npmjs.org/${packageName}/latest`;
|
|
169
|
+
const metadataResponse = await client.webFetch({
|
|
170
|
+
url: metadataUrl,
|
|
171
|
+
maxChars: Math.min(maxChars, 20000)
|
|
172
|
+
});
|
|
173
|
+
if (metadataResponse.status < 200 || metadataResponse.status >= 300) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const metadata = this.tryParseJsonObject(metadataResponse.content);
|
|
177
|
+
if (!metadata) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
const name = this.tryGetString(metadata["name"]) ?? packageName;
|
|
181
|
+
const version = this.tryGetString(metadata["version"]);
|
|
182
|
+
const description = this.tryGetString(metadata["description"]);
|
|
183
|
+
const license = this.tryGetString(metadata["license"]);
|
|
184
|
+
const repositoryUrl = this.tryGetRepositoryUrl(metadata["repository"]);
|
|
185
|
+
const homepageUrl = this.tryGetString(metadata["homepage"]);
|
|
186
|
+
let gitHubRepoUrl = null;
|
|
187
|
+
if (repositoryUrl) {
|
|
188
|
+
gitHubRepoUrl = this.tryNormalizeGitHubRepoUrl(repositoryUrl);
|
|
189
|
+
}
|
|
190
|
+
let readmeText = null;
|
|
191
|
+
let readmeTruncated = false;
|
|
192
|
+
if (gitHubRepoUrl) {
|
|
193
|
+
const readmeResult = await this.tryFetchGitHubReadme(client, gitHubRepoUrl, maxChars);
|
|
194
|
+
if (readmeResult) {
|
|
195
|
+
readmeText = readmeResult.content;
|
|
196
|
+
readmeTruncated = readmeResult.truncated;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const lines = [];
|
|
200
|
+
lines.push(`# ${name}`);
|
|
201
|
+
lines.push("");
|
|
202
|
+
lines.push(`Requested URL: ${params.url}`);
|
|
203
|
+
lines.push("");
|
|
204
|
+
if (description) {
|
|
205
|
+
lines.push(`Description: ${description}`);
|
|
206
|
+
}
|
|
207
|
+
if (version) {
|
|
208
|
+
lines.push(`Latest version: ${version}`);
|
|
209
|
+
}
|
|
210
|
+
if (license) {
|
|
211
|
+
lines.push(`License: ${license}`);
|
|
212
|
+
}
|
|
213
|
+
if (homepageUrl) {
|
|
214
|
+
lines.push(`Homepage: ${homepageUrl}`);
|
|
215
|
+
}
|
|
216
|
+
if (gitHubRepoUrl) {
|
|
217
|
+
lines.push(`Repository: ${gitHubRepoUrl}`);
|
|
218
|
+
}
|
|
219
|
+
else if (repositoryUrl) {
|
|
220
|
+
lines.push(`Repository: ${repositoryUrl}`);
|
|
221
|
+
}
|
|
222
|
+
if (readmeText) {
|
|
223
|
+
lines.push("");
|
|
224
|
+
lines.push("## README");
|
|
225
|
+
lines.push("");
|
|
226
|
+
lines.push(readmeText);
|
|
227
|
+
}
|
|
228
|
+
const combined = lines.join("\n").trim() + "\n";
|
|
229
|
+
const shouldTrim = combined.length > maxChars;
|
|
230
|
+
const content = shouldTrim ? combined.slice(0, maxChars) : combined;
|
|
231
|
+
return {
|
|
232
|
+
content,
|
|
233
|
+
status: 200,
|
|
234
|
+
content_type: "text/markdown",
|
|
235
|
+
truncated: shouldTrim || readmeTruncated || metadataResponse.truncated,
|
|
236
|
+
url: params.url
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Attempts to parse an npm package name from an npmjs.com package page URL.
|
|
241
|
+
*
|
|
242
|
+
* @param url - Parsed URL
|
|
243
|
+
* @returns npm package name (e.g. "chalk" or "@scope/name") or null
|
|
244
|
+
*/
|
|
245
|
+
tryParseNpmPackageName(url) {
|
|
246
|
+
const hostname = url.hostname.toLowerCase();
|
|
247
|
+
if (hostname !== "www.npmjs.com" && hostname !== "npmjs.com") {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
251
|
+
if (segments.length < 2) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
if (segments[0] !== "package") {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
const first = segments[1];
|
|
258
|
+
if (!first) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
if (first.startsWith("@")) {
|
|
262
|
+
const second = segments[2];
|
|
263
|
+
if (!second) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
return `${first}/${second}`;
|
|
267
|
+
}
|
|
268
|
+
return first;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Tries to parse a JSON object from a string.
|
|
272
|
+
*
|
|
273
|
+
* @param input - JSON string
|
|
274
|
+
* @returns Parsed object or null
|
|
275
|
+
*/
|
|
276
|
+
tryParseJsonObject(input) {
|
|
277
|
+
try {
|
|
278
|
+
const parsed = JSON.parse(input);
|
|
279
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
return parsed;
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Extracts a string from an unknown value if possible.
|
|
290
|
+
*
|
|
291
|
+
* @param value - Unknown input
|
|
292
|
+
* @returns Trimmed string or null
|
|
293
|
+
*/
|
|
294
|
+
tryGetString(value) {
|
|
295
|
+
if (typeof value !== "string") {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
const trimmed = value.trim();
|
|
299
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Extracts a repository URL from npm metadata.
|
|
303
|
+
*
|
|
304
|
+
* @param repository - Repository field value
|
|
305
|
+
* @returns Normalized URL string or null
|
|
306
|
+
*/
|
|
307
|
+
tryGetRepositoryUrl(repository) {
|
|
308
|
+
if (typeof repository === "string") {
|
|
309
|
+
return this.normalizeRepositoryUrl(repository);
|
|
310
|
+
}
|
|
311
|
+
if (typeof repository === "object" && repository !== null && !Array.isArray(repository)) {
|
|
312
|
+
const record = repository;
|
|
313
|
+
const rawUrl = this.tryGetString(record["url"]);
|
|
314
|
+
if (!rawUrl) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
return this.normalizeRepositoryUrl(rawUrl);
|
|
318
|
+
}
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Normalizes common git repository URL schemes into an https URL.
|
|
323
|
+
*
|
|
324
|
+
* @param rawUrl - Raw repository URL from metadata
|
|
325
|
+
* @returns Normalized URL string or null
|
|
326
|
+
*/
|
|
327
|
+
normalizeRepositoryUrl(rawUrl) {
|
|
328
|
+
let urlText = rawUrl.trim();
|
|
329
|
+
if (urlText.startsWith("git+")) {
|
|
330
|
+
urlText = urlText.slice("git+".length);
|
|
331
|
+
}
|
|
332
|
+
if (urlText.startsWith("git://")) {
|
|
333
|
+
urlText = `https://${urlText.slice("git://".length)}`;
|
|
334
|
+
}
|
|
335
|
+
if (urlText.endsWith(".git")) {
|
|
336
|
+
urlText = urlText.slice(0, -".git".length);
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
const parsed = new URL(urlText);
|
|
340
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
return parsed.toString();
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Normalizes a GitHub repository URL to the canonical https form.
|
|
351
|
+
*
|
|
352
|
+
* @param repositoryUrl - Repository URL
|
|
353
|
+
* @returns Canonical GitHub repo URL (https://github.com/{owner}/{repo}) or null
|
|
354
|
+
*/
|
|
355
|
+
tryNormalizeGitHubRepoUrl(repositoryUrl) {
|
|
356
|
+
try {
|
|
357
|
+
const parsed = new URL(repositoryUrl);
|
|
358
|
+
if (parsed.hostname.toLowerCase() !== "github.com") {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
362
|
+
if (segments.length < 2) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
const owner = segments[0];
|
|
366
|
+
const repo = segments[1];
|
|
367
|
+
if (!owner || !repo) {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
return `https://github.com/${owner}/${repo}`;
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Attempts to fetch a GitHub repository README via raw.githubusercontent.com.
|
|
378
|
+
*
|
|
379
|
+
* @param client - EnriProxy client
|
|
380
|
+
* @param githubRepoUrl - Canonical GitHub repo URL
|
|
381
|
+
* @param maxChars - Maximum content length
|
|
382
|
+
* @returns README content if found, otherwise null
|
|
383
|
+
*/
|
|
384
|
+
async tryFetchGitHubReadme(client, githubRepoUrl, maxChars) {
|
|
385
|
+
const parsed = new URL(githubRepoUrl);
|
|
386
|
+
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
387
|
+
if (segments.length < 2) {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
const owner = segments[0];
|
|
391
|
+
const repo = segments[1];
|
|
392
|
+
if (!owner || !repo) {
|
|
393
|
+
return null;
|
|
394
|
+
}
|
|
395
|
+
for (const branch of WebFetchTool.README_BRANCHES) {
|
|
396
|
+
for (const filename of WebFetchTool.README_FILENAMES) {
|
|
397
|
+
const url = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${filename}`;
|
|
398
|
+
const response = await client.webFetch({
|
|
399
|
+
url,
|
|
400
|
+
maxChars
|
|
401
|
+
});
|
|
402
|
+
if (response.status >= 200 && response.status < 300 && response.content.trim().length > 0) {
|
|
403
|
+
return {
|
|
404
|
+
content: response.content,
|
|
405
|
+
truncated: response.truncated
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Formats results for MCP text output.
|
|
414
|
+
*
|
|
415
|
+
* @param result - Tool result
|
|
416
|
+
* @returns Formatted text
|
|
417
|
+
*/
|
|
418
|
+
formatOutput(result) {
|
|
419
|
+
const truncatedNote = result.truncated ? " [TRUNCATED]" : "";
|
|
420
|
+
const previewChars = Math.min(DEFAULT_TEXT_PREVIEW_CHARS, result.content.length);
|
|
421
|
+
const preview = result.content.slice(0, previewChars);
|
|
422
|
+
const header = `Fetched ${result.url} (${result.content_type}, ${result.content.length} chars)${truncatedNote}.`;
|
|
423
|
+
const previewNote = previewChars < result.content.length
|
|
424
|
+
? `\n\nPreview (first ${previewChars} chars):\n\n`
|
|
425
|
+
: "\n\nContent:\n\n";
|
|
426
|
+
return header + previewNote + preview;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
//# sourceMappingURL=WebFetchTool.js.map
|