mcp-apitools 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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +78 -0
  3. package/index.js +308 -0
  4. package/package.json +38 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hong Teoh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # mcp-apitools
2
+
3
+ MCP server with 8 API & web development utilities for Claude, Cursor, and other MCP-compatible AI assistants.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npx mcp-apitools
9
+ ```
10
+
11
+ ### Claude Desktop
12
+
13
+ Add to `claude_desktop_config.json`:
14
+
15
+ ```json
16
+ {
17
+ "mcpServers": {
18
+ "apitools": {
19
+ "command": "npx",
20
+ "args": ["-y", "mcp-apitools"]
21
+ }
22
+ }
23
+ }
24
+ ```
25
+
26
+ ### Cursor
27
+
28
+ Add to MCP settings:
29
+
30
+ ```json
31
+ {
32
+ "mcpServers": {
33
+ "apitools": {
34
+ "command": "npx",
35
+ "args": ["-y", "mcp-apitools"]
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ ## Tools (8)
42
+
43
+ | Tool | Description |
44
+ |------|-------------|
45
+ | `http_status` | Look up any HTTP status code — phrase, category, description |
46
+ | `mime_lookup` | Get MIME type for file extension or find extensions for a MIME type |
47
+ | `jwt_create` | Create unsigned JWTs for testing with custom claims |
48
+ | `mock_data` | Generate fake people with names, emails, phones, addresses |
49
+ | `cors_headers` | Generate CORS response headers for your API |
50
+ | `cookie_parse` | Parse Cookie or Set-Cookie headers into structured data |
51
+ | `basic_auth` | Generate Basic Authorization headers |
52
+ | `query_string` | Parse or build URL query strings |
53
+
54
+ ## Examples
55
+
56
+ **Look up HTTP 429:**
57
+ > "What does HTTP 429 mean?" → `Too Many Requests — User has sent too many requests (rate limiting).`
58
+
59
+ **Generate test data:**
60
+ > "Generate 3 fake users for testing" → Returns 3 people with realistic names, emails, addresses
61
+
62
+ **Create a test JWT:**
63
+ > "Make a JWT with sub=123 and role=admin" → Returns unsigned JWT token
64
+
65
+ **Parse cookies:**
66
+ > "Parse this cookie header: session=abc123; theme=dark" → `{"session": "abc123", "theme": "dark"}`
67
+
68
+ ## See Also
69
+
70
+ - [mcp-devutils](https://www.npmjs.com/package/mcp-devutils) — UUID, hash, base64, timestamps, JWT decode, and more
71
+
72
+ ## License
73
+
74
+ MIT
75
+
76
+ ---
77
+
78
+ ☕ If this saves you time, [buy me a coffee](https://buymeacoffee.com/gl89tu25lp)
package/index.js 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 { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
6
+ import crypto from "crypto";
7
+
8
+ const server = new Server(
9
+ { name: "mcp-apitools", version: "1.0.0" },
10
+ { capabilities: { tools: {} } }
11
+ );
12
+
13
+ // ---------- HTTP Status Codes ----------
14
+ const HTTP_STATUSES = {
15
+ 100: ["Continue", "Server received request headers; client should proceed to send body."],
16
+ 101: ["Switching Protocols", "Server is switching protocols as requested (e.g. WebSocket upgrade)."],
17
+ 200: ["OK", "Request succeeded."],
18
+ 201: ["Created", "Request succeeded and a new resource was created."],
19
+ 202: ["Accepted", "Request accepted for processing, but not yet completed."],
20
+ 204: ["No Content", "Request succeeded but there is no content to return."],
21
+ 206: ["Partial Content", "Server is delivering part of the resource due to a Range header."],
22
+ 301: ["Moved Permanently", "Resource has been permanently moved to a new URL."],
23
+ 302: ["Found", "Resource temporarily resides at a different URL."],
24
+ 304: ["Not Modified", "Resource has not been modified since last request (caching)."],
25
+ 307: ["Temporary Redirect", "Request should be repeated with another URL, preserving method."],
26
+ 308: ["Permanent Redirect", "Like 301 but preserves HTTP method."],
27
+ 400: ["Bad Request", "Server cannot process the request due to client error (malformed syntax, invalid parameters)."],
28
+ 401: ["Unauthorized", "Authentication is required and has failed or not been provided."],
29
+ 403: ["Forbidden", "Server understood the request but refuses to authorize it."],
30
+ 404: ["Not Found", "Requested resource could not be found."],
31
+ 405: ["Method Not Allowed", "HTTP method is not allowed for this resource."],
32
+ 406: ["Not Acceptable", "Server cannot produce a response matching the Accept headers."],
33
+ 408: ["Request Timeout", "Server timed out waiting for the request."],
34
+ 409: ["Conflict", "Request conflicts with current state of the resource."],
35
+ 410: ["Gone", "Resource is no longer available and will not be available again."],
36
+ 413: ["Payload Too Large", "Request entity is larger than server is willing to process."],
37
+ 415: ["Unsupported Media Type", "Media type of the request is not supported."],
38
+ 418: ["I'm a Teapot", "RFC 2324 — server refuses to brew coffee because it is a teapot."],
39
+ 422: ["Unprocessable Entity", "Request was well-formed but contained semantic errors."],
40
+ 429: ["Too Many Requests", "User has sent too many requests (rate limiting)."],
41
+ 451: ["Unavailable For Legal Reasons", "Resource unavailable due to legal demands."],
42
+ 500: ["Internal Server Error", "Server encountered an unexpected condition."],
43
+ 502: ["Bad Gateway", "Server received an invalid response from upstream server."],
44
+ 503: ["Service Unavailable", "Server is temporarily unavailable (overloaded or maintenance)."],
45
+ 504: ["Gateway Timeout", "Server did not receive a timely response from upstream."],
46
+ };
47
+
48
+ // ---------- MIME Types ----------
49
+ const MIME_MAP = {
50
+ html: "text/html", htm: "text/html", css: "text/css", js: "application/javascript",
51
+ mjs: "application/javascript", json: "application/json", xml: "application/xml",
52
+ csv: "text/csv", txt: "text/plain", md: "text/markdown",
53
+ png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif",
54
+ svg: "image/svg+xml", webp: "image/webp", ico: "image/x-icon", avif: "image/avif",
55
+ mp3: "audio/mpeg", wav: "audio/wav", ogg: "audio/ogg", mp4: "video/mp4",
56
+ webm: "video/webm", pdf: "application/pdf", zip: "application/zip",
57
+ gz: "application/gzip", tar: "application/x-tar",
58
+ woff: "font/woff", woff2: "font/woff2", ttf: "font/ttf", otf: "font/otf",
59
+ wasm: "application/wasm", yaml: "application/x-yaml", yml: "application/x-yaml",
60
+ toml: "application/toml", ts: "application/typescript", tsx: "application/typescript",
61
+ jsx: "application/javascript", graphql: "application/graphql",
62
+ };
63
+
64
+ // ---------- Mock Data Helpers ----------
65
+ const FIRST_NAMES = ["Alice","Bob","Charlie","Diana","Eve","Frank","Grace","Hank","Ivy","Jack","Karen","Leo","Mia","Noah","Olivia","Pete","Quinn","Rosa","Sam","Tina"];
66
+ const LAST_NAMES = ["Smith","Johnson","Williams","Brown","Jones","Garcia","Miller","Davis","Wilson","Moore","Taylor","Anderson","Thomas","Jackson","White","Harris","Martin","Clark","Lewis","Lee"];
67
+ const DOMAINS = ["example.com","test.org","demo.io","mail.test","sample.net"];
68
+ const STREETS = ["Main St","Oak Ave","Elm Dr","Park Blvd","Cedar Ln","Pine Rd","Maple Way","Lake Dr"];
69
+ const CITIES = ["Springfield","Portland","Madison","Georgetown","Fairview","Salem","Riverside","Clinton"];
70
+ const STATES = ["CA","NY","TX","FL","WA","IL","PA","OH"];
71
+
72
+ function pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; }
73
+ function randInt(a, b) { return Math.floor(Math.random() * (b - a + 1)) + a; }
74
+
75
+ function makePerson() {
76
+ const first = pick(FIRST_NAMES), last = pick(LAST_NAMES);
77
+ return {
78
+ id: crypto.randomUUID(),
79
+ name: `${first} ${last}`,
80
+ email: `${first.toLowerCase()}.${last.toLowerCase()}@${pick(DOMAINS)}`,
81
+ phone: `+1-${randInt(200,999)}-${randInt(100,999)}-${randInt(1000,9999)}`,
82
+ address: {
83
+ street: `${randInt(1,9999)} ${pick(STREETS)}`,
84
+ city: pick(CITIES),
85
+ state: pick(STATES),
86
+ zip: String(randInt(10000,99999)),
87
+ },
88
+ };
89
+ }
90
+
91
+ // ---------- Tool Definitions ----------
92
+ const TOOLS = [
93
+ {
94
+ name: "http_status",
95
+ description: "Look up an HTTP status code — returns phrase, category, and description",
96
+ inputSchema: {
97
+ type: "object",
98
+ properties: {
99
+ code: { type: "number", description: "HTTP status code (e.g. 404)" }
100
+ },
101
+ required: ["code"]
102
+ }
103
+ },
104
+ {
105
+ name: "mime_lookup",
106
+ description: "Get the MIME type for a file extension, or find extensions for a MIME type",
107
+ inputSchema: {
108
+ type: "object",
109
+ properties: {
110
+ query: { type: "string", description: "File extension (e.g. 'png') or MIME type (e.g. 'application/json')" }
111
+ },
112
+ required: ["query"]
113
+ }
114
+ },
115
+ {
116
+ name: "jwt_create",
117
+ description: "Create an unsigned JWT token for testing with custom payload claims",
118
+ inputSchema: {
119
+ type: "object",
120
+ properties: {
121
+ payload: { type: "object", description: "JWT payload claims as key-value pairs" },
122
+ expiresInSeconds: { type: "number", description: "Optional expiration in seconds from now" }
123
+ },
124
+ required: ["payload"]
125
+ }
126
+ },
127
+ {
128
+ name: "mock_data",
129
+ description: "Generate mock/fake people with names, emails, phones, and addresses for testing",
130
+ inputSchema: {
131
+ type: "object",
132
+ properties: {
133
+ count: { type: "number", description: "Number of records to generate (1-50, default 5)" }
134
+ }
135
+ }
136
+ },
137
+ {
138
+ name: "cors_headers",
139
+ description: "Generate CORS response headers for a given origin and methods",
140
+ inputSchema: {
141
+ type: "object",
142
+ properties: {
143
+ origin: { type: "string", description: "Allowed origin (default '*')" },
144
+ methods: { type: "array", items: { type: "string" }, description: "Allowed HTTP methods" },
145
+ allowCredentials: { type: "boolean", description: "Allow credentials (default false)" },
146
+ maxAge: { type: "number", description: "Preflight cache seconds (default 86400)" }
147
+ }
148
+ }
149
+ },
150
+ {
151
+ name: "cookie_parse",
152
+ description: "Parse a Cookie or Set-Cookie header string into structured key-value data",
153
+ inputSchema: {
154
+ type: "object",
155
+ properties: {
156
+ header: { type: "string", description: "Cookie header string (e.g. 'name=value; name2=value2')" }
157
+ },
158
+ required: ["header"]
159
+ }
160
+ },
161
+ {
162
+ name: "basic_auth",
163
+ description: "Generate a Basic Authorization header from username and password",
164
+ inputSchema: {
165
+ type: "object",
166
+ properties: {
167
+ username: { type: "string", description: "Username" },
168
+ password: { type: "string", description: "Password" }
169
+ },
170
+ required: ["username", "password"]
171
+ }
172
+ },
173
+ {
174
+ name: "query_string",
175
+ description: "Parse a URL query string into key-value pairs, or build one from a JSON object",
176
+ inputSchema: {
177
+ type: "object",
178
+ properties: {
179
+ input: { type: "string", description: "URL/query string to parse, or JSON object to encode" }
180
+ },
181
+ required: ["input"]
182
+ }
183
+ }
184
+ ];
185
+
186
+ // ---------- Handlers ----------
187
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
188
+
189
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
190
+ const { name, arguments: args } = request.params;
191
+
192
+ switch (name) {
193
+ case "http_status": {
194
+ const code = args.code;
195
+ const cat = code < 200 ? "Informational" : code < 300 ? "Success" : code < 400 ? "Redirection" : code < 500 ? "Client Error" : "Server Error";
196
+ const info = HTTP_STATUSES[code];
197
+ const text = info
198
+ ? `${code} ${info[0]}\nCategory: ${cat}\n${info[1]}`
199
+ : `${code} — ${cat} (no standard definition for this code)`;
200
+ return { content: [{ type: "text", text }] };
201
+ }
202
+
203
+ case "mime_lookup": {
204
+ const q = (args.query || "").replace(/^\./, "").toLowerCase();
205
+ if (MIME_MAP[q]) {
206
+ return { content: [{ type: "text", text: `.${q} → ${MIME_MAP[q]}` }] };
207
+ }
208
+ const exts = Object.entries(MIME_MAP).filter(([, m]) => m === q).map(([e]) => `.${e}`);
209
+ if (exts.length) {
210
+ return { content: [{ type: "text", text: `${q} → ${exts.join(", ")}` }] };
211
+ }
212
+ return { content: [{ type: "text", text: `No match for "${args.query}". Try extension (e.g. 'png') or MIME type (e.g. 'image/png').` }] };
213
+ }
214
+
215
+ case "jwt_create": {
216
+ const header = { alg: "none", typ: "JWT" };
217
+ const now = Math.floor(Date.now() / 1000);
218
+ const claims = { iat: now, ...args.payload };
219
+ if (args.expiresInSeconds) claims.exp = now + args.expiresInSeconds;
220
+ const b64 = (o) => Buffer.from(JSON.stringify(o)).toString("base64url");
221
+ const token = `${b64(header)}.${b64(claims)}.`;
222
+ return { content: [{ type: "text", text: `${token}\n\nHeader: ${JSON.stringify(header)}\nPayload: ${JSON.stringify(claims, null, 2)}\n\n⚠️ Unsigned token (alg: none) — for testing only.` }] };
223
+ }
224
+
225
+ case "mock_data": {
226
+ const count = Math.max(1, Math.min(50, args.count || 5));
227
+ const people = Array.from({ length: count }, makePerson);
228
+ return { content: [{ type: "text", text: JSON.stringify(people, null, 2) }] };
229
+ }
230
+
231
+ case "cors_headers": {
232
+ const origin = args.origin || "*";
233
+ const methods = args.methods || ["GET", "POST", "PUT", "DELETE", "OPTIONS"];
234
+ const creds = args.allowCredentials || false;
235
+ const maxAge = args.maxAge || 86400;
236
+ if (creds && origin === "*") {
237
+ return { content: [{ type: "text", text: "⚠️ Cannot use Access-Control-Allow-Credentials with origin '*'. Specify an explicit origin." }] };
238
+ }
239
+ const h = {
240
+ "Access-Control-Allow-Origin": origin,
241
+ "Access-Control-Allow-Methods": methods.join(", "),
242
+ "Access-Control-Allow-Headers": "Content-Type, Authorization, X-Requested-With",
243
+ "Access-Control-Max-Age": String(maxAge),
244
+ };
245
+ if (creds) h["Access-Control-Allow-Credentials"] = "true";
246
+ const lines = Object.entries(h).map(([k, v]) => `${k}: ${v}`).join("\n");
247
+ return { content: [{ type: "text", text: `CORS Response Headers:\n\n${lines}` }] };
248
+ }
249
+
250
+ case "cookie_parse": {
251
+ const hdr = args.header;
252
+ const isSetCookie = /;\s*(expires|max-age|domain|path|secure|httponly|samesite)\s*[=;]/i.test(hdr);
253
+ if (isSetCookie) {
254
+ const parts = hdr.split(";").map((s) => s.trim());
255
+ const [nameVal, ...attrs] = parts;
256
+ const eq = nameVal.indexOf("=");
257
+ const cName = eq > -1 ? nameVal.slice(0, eq) : nameVal;
258
+ const cValue = eq > -1 ? nameVal.slice(eq + 1) : "";
259
+ const attributes = {};
260
+ for (const a of attrs) {
261
+ const aEq = a.indexOf("=");
262
+ if (aEq > -1) attributes[a.slice(0, aEq).trim().toLowerCase()] = a.slice(aEq + 1).trim();
263
+ else attributes[a.toLowerCase()] = true;
264
+ }
265
+ return { content: [{ type: "text", text: JSON.stringify({ name: cName, value: cValue, attributes }, null, 2) }] };
266
+ }
267
+ const cookies = {};
268
+ for (const pair of hdr.split(";")) {
269
+ const eq = pair.indexOf("=");
270
+ if (eq > -1) cookies[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
271
+ }
272
+ return { content: [{ type: "text", text: JSON.stringify(cookies, null, 2) }] };
273
+ }
274
+
275
+ case "basic_auth": {
276
+ const encoded = Buffer.from(`${args.username}:${args.password}`).toString("base64");
277
+ return { content: [{ type: "text", text: `Authorization: Basic ${encoded}\n\nDecoded: ${args.username}:${args.password}\n\n⚠️ Basic auth transmits credentials in base64 (NOT encrypted). Always use HTTPS.` }] };
278
+ }
279
+
280
+ case "query_string": {
281
+ const input = args.input;
282
+ try {
283
+ const obj = JSON.parse(input);
284
+ if (typeof obj === "object" && obj !== null) {
285
+ const params = new URLSearchParams();
286
+ for (const [k, v] of Object.entries(obj)) params.append(k, String(v));
287
+ return { content: [{ type: "text", text: `Query string: ?${params.toString()}\n\nParams:\n${JSON.stringify(obj, null, 2)}` }] };
288
+ }
289
+ } catch { /* parse as query string */ }
290
+ const qIdx = input.indexOf("?");
291
+ const qs = qIdx > -1 ? input.slice(qIdx + 1) : input;
292
+ const params = new URLSearchParams(qs);
293
+ const result = {};
294
+ for (const [k, v] of params) {
295
+ if (result[k]) result[k] = Array.isArray(result[k]) ? [...result[k], v] : [result[k], v];
296
+ else result[k] = v;
297
+ }
298
+ return { content: [{ type: "text", text: `Parsed query parameters:\n${JSON.stringify(result, null, 2)}` }] };
299
+ }
300
+
301
+ default:
302
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
303
+ }
304
+ });
305
+
306
+ // ---------- Start ----------
307
+ const transport = new StdioServerTransport();
308
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "mcp-apitools",
3
+ "version": "1.0.0",
4
+ "description": "MCP server with API & web development utilities — HTTP status codes, MIME types, JWT creation, mock data generation, CORS headers, cookie parsing",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "mcp-apitools": "index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node index.js"
12
+ },
13
+ "keywords": [
14
+ "mcp",
15
+ "model-context-protocol",
16
+ "api-tools",
17
+ "http-status",
18
+ "mime-type",
19
+ "jwt",
20
+ "mock-data",
21
+ "cors",
22
+ "cookie",
23
+ "web-development",
24
+ "claude",
25
+ "cursor",
26
+ "developer-tools",
27
+ "rest-api"
28
+ ],
29
+ "author": "Hong Teoh",
30
+ "license": "MIT",
31
+ "funding": {
32
+ "type": "buymeacoffee",
33
+ "url": "https://buymeacoffee.com/gl89tu25lp"
34
+ },
35
+ "dependencies": {
36
+ "@modelcontextprotocol/sdk": "^1.0.0"
37
+ }
38
+ }