@rog0x/mcp-api-tools 1.0.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 +81 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +269 -0
- package/dist/tools/api-health.d.ts +31 -0
- package/dist/tools/api-health.js +167 -0
- package/dist/tools/header-analyzer.d.ts +56 -0
- package/dist/tools/header-analyzer.js +189 -0
- package/dist/tools/http-request.d.ts +29 -0
- package/dist/tools/http-request.js +64 -0
- package/dist/tools/jwt-decode.d.ts +12 -0
- package/dist/tools/jwt-decode.js +71 -0
- package/dist/tools/url-parser.d.ts +27 -0
- package/dist/tools/url-parser.js +73 -0
- package/package.json +26 -0
- package/src/index.ts +308 -0
- package/src/tools/api-health.ts +179 -0
- package/src/tools/header-analyzer.ts +262 -0
- package/src/tools/http-request.ts +112 -0
- package/src/tools/jwt-decode.ts +82 -0
- package/src/tools/url-parser.ts +108 -0
- package/tsconfig.json +18 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import {
|
|
6
|
+
CallToolRequestSchema,
|
|
7
|
+
ListToolsRequestSchema,
|
|
8
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
+
import { httpRequest } from "./tools/http-request.js";
|
|
10
|
+
import { apiHealth } from "./tools/api-health.js";
|
|
11
|
+
import { jwtDecode } from "./tools/jwt-decode.js";
|
|
12
|
+
import { parseUrl, buildUrl } from "./tools/url-parser.js";
|
|
13
|
+
import { analyzeHeaders } from "./tools/header-analyzer.js";
|
|
14
|
+
|
|
15
|
+
const server = new Server(
|
|
16
|
+
{
|
|
17
|
+
name: "mcp-api-tools",
|
|
18
|
+
version: "1.0.0",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
capabilities: {
|
|
22
|
+
tools: {},
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
28
|
+
tools: [
|
|
29
|
+
{
|
|
30
|
+
name: "http_request",
|
|
31
|
+
description:
|
|
32
|
+
"Make an HTTP request with full control over method, headers, body, authentication, and timeouts. Returns status, headers, body, and timing information.",
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: "object" as const,
|
|
35
|
+
properties: {
|
|
36
|
+
method: {
|
|
37
|
+
type: "string",
|
|
38
|
+
description: "HTTP method: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS",
|
|
39
|
+
},
|
|
40
|
+
url: {
|
|
41
|
+
type: "string",
|
|
42
|
+
description: "The full URL to send the request to",
|
|
43
|
+
},
|
|
44
|
+
headers: {
|
|
45
|
+
type: "object",
|
|
46
|
+
description: "Key-value pairs of HTTP headers to include",
|
|
47
|
+
additionalProperties: { type: "string" },
|
|
48
|
+
},
|
|
49
|
+
body: {
|
|
50
|
+
type: "string",
|
|
51
|
+
description: "Request body (for POST, PUT, PATCH)",
|
|
52
|
+
},
|
|
53
|
+
timeout: {
|
|
54
|
+
type: "number",
|
|
55
|
+
description: "Request timeout in milliseconds (default: 30000)",
|
|
56
|
+
},
|
|
57
|
+
follow_redirects: {
|
|
58
|
+
type: "boolean",
|
|
59
|
+
description: "Whether to follow redirects (default: true)",
|
|
60
|
+
},
|
|
61
|
+
auth: {
|
|
62
|
+
type: "object",
|
|
63
|
+
description: "Authentication configuration",
|
|
64
|
+
properties: {
|
|
65
|
+
type: {
|
|
66
|
+
type: "string",
|
|
67
|
+
enum: ["basic", "bearer"],
|
|
68
|
+
description: "Auth type: basic or bearer",
|
|
69
|
+
},
|
|
70
|
+
username: { type: "string", description: "Username for basic auth" },
|
|
71
|
+
password: { type: "string", description: "Password for basic auth" },
|
|
72
|
+
token: { type: "string", description: "Token for bearer auth" },
|
|
73
|
+
},
|
|
74
|
+
required: ["type"],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
required: ["method", "url"],
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: "api_health",
|
|
82
|
+
description:
|
|
83
|
+
"Check the health of multiple API endpoints. Returns status, response time, SSL certificate validity, and optional response body validation for each endpoint.",
|
|
84
|
+
inputSchema: {
|
|
85
|
+
type: "object" as const,
|
|
86
|
+
properties: {
|
|
87
|
+
endpoints: {
|
|
88
|
+
type: "array",
|
|
89
|
+
description: "Array of endpoints to check (max 20)",
|
|
90
|
+
items: {
|
|
91
|
+
type: "object",
|
|
92
|
+
properties: {
|
|
93
|
+
url: { type: "string", description: "Endpoint URL" },
|
|
94
|
+
expected_status: {
|
|
95
|
+
type: "number",
|
|
96
|
+
description: "Expected HTTP status code (default: 200)",
|
|
97
|
+
},
|
|
98
|
+
expected_body_contains: {
|
|
99
|
+
type: "string",
|
|
100
|
+
description: "String the response body must contain",
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
required: ["url"],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
required: ["endpoints"],
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "jwt_decode",
|
|
112
|
+
description:
|
|
113
|
+
"Decode a JWT token without verification. Returns header, payload, expiry, issued-at time, and whether the token is expired.",
|
|
114
|
+
inputSchema: {
|
|
115
|
+
type: "object" as const,
|
|
116
|
+
properties: {
|
|
117
|
+
token: {
|
|
118
|
+
type: "string",
|
|
119
|
+
description: "The JWT token string to decode",
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
required: ["token"],
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: "url_parse",
|
|
127
|
+
description:
|
|
128
|
+
"Parse a URL into its component parts: protocol, host, port, path, query parameters, hash, and more. Can also build a URL from parts.",
|
|
129
|
+
inputSchema: {
|
|
130
|
+
type: "object" as const,
|
|
131
|
+
properties: {
|
|
132
|
+
url: {
|
|
133
|
+
type: "string",
|
|
134
|
+
description: "URL to parse (use this OR build_params, not both)",
|
|
135
|
+
},
|
|
136
|
+
build_params: {
|
|
137
|
+
type: "object",
|
|
138
|
+
description: "Build a URL from parts instead of parsing",
|
|
139
|
+
properties: {
|
|
140
|
+
protocol: { type: "string", description: "Protocol (default: https:)" },
|
|
141
|
+
hostname: { type: "string", description: "Hostname (required)" },
|
|
142
|
+
port: { type: "string", description: "Port number" },
|
|
143
|
+
pathname: { type: "string", description: "Path (default: /)" },
|
|
144
|
+
query_params: {
|
|
145
|
+
type: "object",
|
|
146
|
+
description: "Query parameters as key-value pairs",
|
|
147
|
+
additionalProperties: {
|
|
148
|
+
oneOf: [
|
|
149
|
+
{ type: "string" },
|
|
150
|
+
{ type: "array", items: { type: "string" } },
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
hash: { type: "string", description: "Hash/fragment" },
|
|
155
|
+
username: { type: "string", description: "URL username" },
|
|
156
|
+
password: { type: "string", description: "URL password" },
|
|
157
|
+
},
|
|
158
|
+
required: ["hostname"],
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: "header_analyzer",
|
|
165
|
+
description:
|
|
166
|
+
"Analyze HTTP response headers for security (HSTS, CSP, X-Frame-Options, etc.), caching directives, CORS configuration, cookies, and server information. Provides a security grade.",
|
|
167
|
+
inputSchema: {
|
|
168
|
+
type: "object" as const,
|
|
169
|
+
properties: {
|
|
170
|
+
headers: {
|
|
171
|
+
type: "object",
|
|
172
|
+
description: "HTTP response headers as key-value pairs to analyze",
|
|
173
|
+
additionalProperties: { type: "string" },
|
|
174
|
+
},
|
|
175
|
+
url: {
|
|
176
|
+
type: "string",
|
|
177
|
+
description:
|
|
178
|
+
"Alternatively, provide a URL to fetch and analyze its response headers",
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
}));
|
|
185
|
+
|
|
186
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
187
|
+
const { name, arguments: args } = request.params;
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
switch (name) {
|
|
191
|
+
case "http_request": {
|
|
192
|
+
const result = await httpRequest({
|
|
193
|
+
method: args?.method as string,
|
|
194
|
+
url: args?.url as string,
|
|
195
|
+
headers: args?.headers as Record<string, string> | undefined,
|
|
196
|
+
body: args?.body as string | undefined,
|
|
197
|
+
timeout: args?.timeout as number | undefined,
|
|
198
|
+
follow_redirects: args?.follow_redirects as boolean | undefined,
|
|
199
|
+
auth: args?.auth as {
|
|
200
|
+
type: "basic" | "bearer";
|
|
201
|
+
username?: string;
|
|
202
|
+
password?: string;
|
|
203
|
+
token?: string;
|
|
204
|
+
} | undefined,
|
|
205
|
+
});
|
|
206
|
+
return {
|
|
207
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
case "api_health": {
|
|
212
|
+
const endpoints = args?.endpoints as Array<{
|
|
213
|
+
url: string;
|
|
214
|
+
expected_status?: number;
|
|
215
|
+
expected_body_contains?: string;
|
|
216
|
+
}>;
|
|
217
|
+
const result = await apiHealth(endpoints);
|
|
218
|
+
return {
|
|
219
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
case "jwt_decode": {
|
|
224
|
+
const result = jwtDecode(args?.token as string);
|
|
225
|
+
return {
|
|
226
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
case "url_parse": {
|
|
231
|
+
if (args?.build_params) {
|
|
232
|
+
const built = buildUrl(
|
|
233
|
+
args.build_params as {
|
|
234
|
+
protocol?: string;
|
|
235
|
+
hostname: string;
|
|
236
|
+
port?: string | number;
|
|
237
|
+
pathname?: string;
|
|
238
|
+
query_params?: Record<string, string | string[]>;
|
|
239
|
+
hash?: string;
|
|
240
|
+
username?: string;
|
|
241
|
+
password?: string;
|
|
242
|
+
}
|
|
243
|
+
);
|
|
244
|
+
return {
|
|
245
|
+
content: [
|
|
246
|
+
{ type: "text", text: JSON.stringify({ built_url: built }, null, 2) },
|
|
247
|
+
],
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
if (args?.url) {
|
|
251
|
+
const parsed = parseUrl(args.url as string);
|
|
252
|
+
return {
|
|
253
|
+
content: [{ type: "text", text: JSON.stringify(parsed, null, 2) }],
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
throw new Error("Provide either 'url' to parse or 'build_params' to build");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
case "header_analyzer": {
|
|
260
|
+
if (args?.url) {
|
|
261
|
+
const controller = new AbortController();
|
|
262
|
+
const timer = setTimeout(() => controller.abort(), 15000);
|
|
263
|
+
try {
|
|
264
|
+
const response = await fetch(args.url as string, {
|
|
265
|
+
method: "HEAD",
|
|
266
|
+
signal: controller.signal,
|
|
267
|
+
redirect: "follow",
|
|
268
|
+
});
|
|
269
|
+
const fetchedHeaders: Record<string, string> = {};
|
|
270
|
+
response.headers.forEach((value, key) => {
|
|
271
|
+
fetchedHeaders[key] = value;
|
|
272
|
+
});
|
|
273
|
+
const result = analyzeHeaders(fetchedHeaders);
|
|
274
|
+
return {
|
|
275
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
276
|
+
};
|
|
277
|
+
} finally {
|
|
278
|
+
clearTimeout(timer);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (args?.headers) {
|
|
282
|
+
const result = analyzeHeaders(args.headers as Record<string, string>);
|
|
283
|
+
return {
|
|
284
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
throw new Error("Provide either 'headers' object or 'url' to fetch headers from");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
default:
|
|
291
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
292
|
+
}
|
|
293
|
+
} catch (error: unknown) {
|
|
294
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
295
|
+
return {
|
|
296
|
+
content: [{ type: "text", text: `Error: ${message}` }],
|
|
297
|
+
isError: true,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
async function main() {
|
|
303
|
+
const transport = new StdioServerTransport();
|
|
304
|
+
await server.connect(transport);
|
|
305
|
+
console.error("MCP API Tools server running on stdio");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import * as tls from "node:tls";
|
|
2
|
+
import * as https from "node:https";
|
|
3
|
+
|
|
4
|
+
export interface HealthEndpoint {
|
|
5
|
+
url: string;
|
|
6
|
+
expected_status?: number;
|
|
7
|
+
expected_body_contains?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface EndpointHealth {
|
|
11
|
+
url: string;
|
|
12
|
+
status: "healthy" | "unhealthy" | "error";
|
|
13
|
+
http_status: number | null;
|
|
14
|
+
response_time_ms: number;
|
|
15
|
+
ssl: SslInfo | null;
|
|
16
|
+
body_valid: boolean | null;
|
|
17
|
+
error: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SslInfo {
|
|
21
|
+
valid: boolean;
|
|
22
|
+
issuer: string;
|
|
23
|
+
subject: string;
|
|
24
|
+
expires: string;
|
|
25
|
+
days_remaining: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function checkSsl(hostname: string, port: number): Promise<SslInfo | null> {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
const timeout = setTimeout(() => {
|
|
31
|
+
socket.destroy();
|
|
32
|
+
resolve(null);
|
|
33
|
+
}, 5000);
|
|
34
|
+
|
|
35
|
+
const socket = tls.connect({ host: hostname, port, servername: hostname }, () => {
|
|
36
|
+
clearTimeout(timeout);
|
|
37
|
+
const cert = socket.getPeerCertificate();
|
|
38
|
+
if (!cert || !cert.valid_to) {
|
|
39
|
+
socket.destroy();
|
|
40
|
+
resolve(null);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const expiresDate = new Date(cert.valid_to);
|
|
45
|
+
const now = new Date();
|
|
46
|
+
const daysRemaining = Math.floor(
|
|
47
|
+
(expiresDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
const flatten = (val: string | string[] | undefined): string => {
|
|
51
|
+
if (!val) return "Unknown";
|
|
52
|
+
return Array.isArray(val) ? val[0] : val;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
resolve({
|
|
56
|
+
valid: socket.authorized,
|
|
57
|
+
issuer:
|
|
58
|
+
typeof cert.issuer === "object"
|
|
59
|
+
? flatten(cert.issuer.O) !== "Unknown"
|
|
60
|
+
? flatten(cert.issuer.O)
|
|
61
|
+
: flatten(cert.issuer.CN)
|
|
62
|
+
: "Unknown",
|
|
63
|
+
subject:
|
|
64
|
+
typeof cert.subject === "object"
|
|
65
|
+
? flatten(cert.subject.CN)
|
|
66
|
+
: "Unknown",
|
|
67
|
+
expires: cert.valid_to,
|
|
68
|
+
days_remaining: daysRemaining,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
socket.destroy();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
socket.on("error", () => {
|
|
75
|
+
clearTimeout(timeout);
|
|
76
|
+
resolve(null);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function checkEndpoint(endpoint: HealthEndpoint): Promise<EndpointHealth> {
|
|
82
|
+
const { url, expected_status = 200, expected_body_contains } = endpoint;
|
|
83
|
+
|
|
84
|
+
let parsedUrl: URL;
|
|
85
|
+
try {
|
|
86
|
+
parsedUrl = new URL(url);
|
|
87
|
+
} catch {
|
|
88
|
+
return {
|
|
89
|
+
url,
|
|
90
|
+
status: "error",
|
|
91
|
+
http_status: null,
|
|
92
|
+
response_time_ms: 0,
|
|
93
|
+
ssl: null,
|
|
94
|
+
body_valid: null,
|
|
95
|
+
error: "Invalid URL",
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const isHttps = parsedUrl.protocol === "https:";
|
|
100
|
+
const sslPromise = isHttps
|
|
101
|
+
? checkSsl(parsedUrl.hostname, Number(parsedUrl.port) || 443)
|
|
102
|
+
: Promise.resolve(null);
|
|
103
|
+
|
|
104
|
+
const controller = new AbortController();
|
|
105
|
+
const timer = setTimeout(() => controller.abort(), 15000);
|
|
106
|
+
const start = Date.now();
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const response = await fetch(url, {
|
|
110
|
+
method: "GET",
|
|
111
|
+
signal: controller.signal,
|
|
112
|
+
redirect: "follow",
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const body = await response.text();
|
|
116
|
+
const elapsed = Date.now() - start;
|
|
117
|
+
|
|
118
|
+
let bodyValid: boolean | null = null;
|
|
119
|
+
if (expected_body_contains) {
|
|
120
|
+
bodyValid = body.includes(expected_body_contains);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const isHealthy =
|
|
124
|
+
response.status === expected_status &&
|
|
125
|
+
(bodyValid === null || bodyValid === true);
|
|
126
|
+
|
|
127
|
+
const ssl = await sslPromise;
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
url,
|
|
131
|
+
status: isHealthy ? "healthy" : "unhealthy",
|
|
132
|
+
http_status: response.status,
|
|
133
|
+
response_time_ms: elapsed,
|
|
134
|
+
ssl,
|
|
135
|
+
body_valid: bodyValid,
|
|
136
|
+
error: null,
|
|
137
|
+
};
|
|
138
|
+
} catch (err: unknown) {
|
|
139
|
+
const elapsed = Date.now() - start;
|
|
140
|
+
const ssl = await sslPromise;
|
|
141
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
142
|
+
return {
|
|
143
|
+
url,
|
|
144
|
+
status: "error",
|
|
145
|
+
http_status: null,
|
|
146
|
+
response_time_ms: elapsed,
|
|
147
|
+
ssl,
|
|
148
|
+
body_valid: null,
|
|
149
|
+
error: message,
|
|
150
|
+
};
|
|
151
|
+
} finally {
|
|
152
|
+
clearTimeout(timer);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function apiHealth(
|
|
157
|
+
endpoints: HealthEndpoint[]
|
|
158
|
+
): Promise<{ results: EndpointHealth[]; summary: { total: number; healthy: number; unhealthy: number; errors: number } }> {
|
|
159
|
+
if (!endpoints || endpoints.length === 0) {
|
|
160
|
+
throw new Error("Provide at least one endpoint to check");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const capped = endpoints.slice(0, 20);
|
|
164
|
+
const results = await Promise.all(capped.map(checkEndpoint));
|
|
165
|
+
|
|
166
|
+
const healthy = results.filter((r) => r.status === "healthy").length;
|
|
167
|
+
const unhealthy = results.filter((r) => r.status === "unhealthy").length;
|
|
168
|
+
const errors = results.filter((r) => r.status === "error").length;
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
results,
|
|
172
|
+
summary: {
|
|
173
|
+
total: results.length,
|
|
174
|
+
healthy,
|
|
175
|
+
unhealthy,
|
|
176
|
+
errors,
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|