it-tools-mcp 3.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.
@@ -0,0 +1,407 @@
1
+ import { z } from "zod";
2
+ import mimeTypes from 'mime-types';
3
+ export function registerUtilityTools(server) {
4
+ // Email normalizer
5
+ server.tool("email-normalizer", "Normalize email addresses (remove dots, plus aliases, etc.)", {
6
+ email: z.string().describe("Email address to normalize"),
7
+ }, async ({ email }) => {
8
+ try {
9
+ // Basic email validation
10
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
11
+ if (!emailRegex.test(email)) {
12
+ throw new Error("Invalid email format");
13
+ }
14
+ const [localPart, domain] = email.toLowerCase().split('@');
15
+ let normalizedLocal = localPart;
16
+ let notes = [];
17
+ // Handle Gmail-specific rules
18
+ if (domain === 'gmail.com' || domain === 'googlemail.com') {
19
+ // Remove dots from Gmail addresses
20
+ const withoutDots = normalizedLocal.replace(/\./g, '');
21
+ if (withoutDots !== normalizedLocal) {
22
+ notes.push(`Removed dots: ${normalizedLocal} → ${withoutDots}`);
23
+ normalizedLocal = withoutDots;
24
+ }
25
+ // Remove everything after + (alias)
26
+ const plusIndex = normalizedLocal.indexOf('+');
27
+ if (plusIndex !== -1) {
28
+ const withoutAlias = normalizedLocal.substring(0, plusIndex);
29
+ notes.push(`Removed alias: ${normalizedLocal} → ${withoutAlias}`);
30
+ normalizedLocal = withoutAlias;
31
+ }
32
+ }
33
+ // Handle other common plus aliasing
34
+ const plusIndex = normalizedLocal.indexOf('+');
35
+ if (plusIndex !== -1 && domain !== 'gmail.com' && domain !== 'googlemail.com') {
36
+ const withoutAlias = normalizedLocal.substring(0, plusIndex);
37
+ notes.push(`Removed plus alias: ${normalizedLocal} → ${withoutAlias}`);
38
+ normalizedLocal = withoutAlias;
39
+ }
40
+ const normalizedEmail = `${normalizedLocal}@${domain}`;
41
+ const isChanged = normalizedEmail !== email.toLowerCase();
42
+ return {
43
+ content: [
44
+ {
45
+ type: "text",
46
+ text: `Email Normalization:
47
+
48
+ Original: ${email}
49
+ Normalized: ${normalizedEmail}
50
+ Changed: ${isChanged ? 'Yes' : 'No'}
51
+
52
+ ${notes.length > 0 ? `Transformations applied:
53
+ ${notes.map(note => `• ${note}`).join('\n')}` : 'No transformations needed'}
54
+
55
+ Domain: ${domain}
56
+ Local part: ${normalizedLocal}
57
+
58
+ Note: This normalization helps identify duplicate email addresses
59
+ that would be delivered to the same inbox.`,
60
+ },
61
+ ],
62
+ };
63
+ }
64
+ catch (error) {
65
+ return {
66
+ content: [
67
+ {
68
+ type: "text",
69
+ text: `Error normalizing email: ${error instanceof Error ? error.message : 'Unknown error'}`,
70
+ },
71
+ ],
72
+ };
73
+ }
74
+ });
75
+ // MIME type lookup
76
+ server.tool("mime-types", "Look up MIME types for file extensions", {
77
+ input: z.string().describe("File extension (e.g., 'txt') or MIME type (e.g., 'text/plain')"),
78
+ lookupType: z.enum(["extension-to-mime", "mime-to-extension"]).describe("Lookup direction").optional(),
79
+ }, async ({ input, lookupType = "extension-to-mime" }) => {
80
+ try {
81
+ if (lookupType === "extension-to-mime") {
82
+ const ext = input.toLowerCase().replace(/^\./, ''); // Remove leading dot if present
83
+ const mimeType = mimeTypes.lookup(ext);
84
+ if (!mimeType) {
85
+ return {
86
+ content: [
87
+ {
88
+ type: "text",
89
+ text: `MIME Type Lookup:
90
+
91
+ Extension: .${ext}
92
+ Result: Not found
93
+
94
+ Note: This extension is not recognized in the MIME types database.`,
95
+ },
96
+ ],
97
+ };
98
+ }
99
+ return {
100
+ content: [
101
+ {
102
+ type: "text",
103
+ text: `MIME Type Lookup:
104
+
105
+ Extension: .${ext}
106
+ MIME Type: ${mimeType}
107
+
108
+ Category: ${getMimeCategory(mimeType)}
109
+ Description: ${getMimeDescription(mimeType)}`,
110
+ },
111
+ ],
112
+ };
113
+ }
114
+ else {
115
+ // mime-to-extension
116
+ const mimeType = input.toLowerCase();
117
+ const extension = mimeTypes.extension(mimeType);
118
+ if (!extension) {
119
+ return {
120
+ content: [
121
+ {
122
+ type: "text",
123
+ text: `Extension Lookup:
124
+
125
+ MIME Type: ${input}
126
+ Result: Not found
127
+
128
+ Note: This MIME type is not recognized or has no standard extension.`,
129
+ },
130
+ ],
131
+ };
132
+ }
133
+ return {
134
+ content: [
135
+ {
136
+ type: "text",
137
+ text: `Extension Lookup:
138
+
139
+ MIME Type: ${mimeType}
140
+ Extension: .${extension}
141
+ Primary: .${extension}
142
+
143
+ Category: ${getMimeCategory(mimeType)}
144
+ Description: ${getMimeDescription(mimeType)}`,
145
+ },
146
+ ],
147
+ };
148
+ }
149
+ }
150
+ catch (error) {
151
+ return {
152
+ content: [
153
+ {
154
+ type: "text",
155
+ text: `Error looking up MIME type: ${error instanceof Error ? error.message : 'Unknown error'}`,
156
+ },
157
+ ],
158
+ };
159
+ }
160
+ });
161
+ // Device info
162
+ server.tool("device-info", "Get basic device/system information", {}, async () => {
163
+ try {
164
+ const info = {
165
+ platform: process.platform,
166
+ arch: process.arch,
167
+ nodeVersion: process.version,
168
+ userAgent: 'IT Tools MCP Server',
169
+ timestamp: new Date().toISOString(),
170
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
171
+ language: 'en-US' // Default for server environment
172
+ };
173
+ return {
174
+ content: [
175
+ {
176
+ type: "text",
177
+ text: `Device Information:
178
+
179
+ Platform: ${info.platform}
180
+ Architecture: ${info.arch}
181
+ Node.js Version: ${info.nodeVersion}
182
+ User Agent: ${info.userAgent}
183
+ Current Time: ${info.timestamp}
184
+ Timezone: ${info.timezone}
185
+ Language: ${info.language}
186
+
187
+ Note: This is basic server environment information.
188
+ For detailed client device info, use a browser-based tool.`,
189
+ },
190
+ ],
191
+ };
192
+ }
193
+ catch (error) {
194
+ return {
195
+ content: [
196
+ {
197
+ type: "text",
198
+ text: `Error getting device info: ${error instanceof Error ? error.message : 'Unknown error'}`,
199
+ },
200
+ ],
201
+ };
202
+ }
203
+ });
204
+ // HTTP status codes reference
205
+ server.tool("http-status-codes", "Get information about HTTP status codes", {
206
+ code: z.number().optional().describe("HTTP status code to look up (optional)"),
207
+ }, async ({ code }) => {
208
+ try {
209
+ const statusCodes = {
210
+ // 1xx Informational
211
+ 100: "Continue",
212
+ 101: "Switching Protocols",
213
+ 102: "Processing",
214
+ // 2xx Success
215
+ 200: "OK",
216
+ 201: "Created",
217
+ 202: "Accepted",
218
+ 204: "No Content",
219
+ 206: "Partial Content",
220
+ // 3xx Redirection
221
+ 300: "Multiple Choices",
222
+ 301: "Moved Permanently",
223
+ 302: "Found",
224
+ 304: "Not Modified",
225
+ 307: "Temporary Redirect",
226
+ 308: "Permanent Redirect",
227
+ // 4xx Client Error
228
+ 400: "Bad Request",
229
+ 401: "Unauthorized",
230
+ 403: "Forbidden",
231
+ 404: "Not Found",
232
+ 405: "Method Not Allowed",
233
+ 409: "Conflict",
234
+ 410: "Gone",
235
+ 422: "Unprocessable Entity",
236
+ 429: "Too Many Requests",
237
+ // 5xx Server Error
238
+ 500: "Internal Server Error",
239
+ 501: "Not Implemented",
240
+ 502: "Bad Gateway",
241
+ 503: "Service Unavailable",
242
+ 504: "Gateway Timeout",
243
+ 505: "HTTP Version Not Supported"
244
+ };
245
+ const descriptions = {
246
+ 100: "The server has received the request headers and the client should proceed to send the request body.",
247
+ 101: "The requester has asked the server to switch protocols and the server has agreed to do so.",
248
+ 102: "The server has received and is processing the request, but no response is available yet.",
249
+ 200: "The request has succeeded.",
250
+ 201: "The request has been fulfilled and resulted in a new resource being created.",
251
+ 202: "The request has been accepted for processing, but the processing has not been completed.",
252
+ 204: "The server successfully processed the request and is not returning any content.",
253
+ 206: "The server is delivering only part of the resource due to a range header sent by the client.",
254
+ 300: "The request has more than one possible response.",
255
+ 301: "The URL of the requested resource has been changed permanently.",
256
+ 302: "The resource is temporarily located at a different URL.",
257
+ 304: "The response has not been modified since the last request.",
258
+ 307: "The request should be repeated with another URI, but future requests should still use the original URI.",
259
+ 308: "The request and all future requests should be repeated using another URI.",
260
+ 400: "The server could not understand the request due to invalid syntax.",
261
+ 401: "The client must authenticate itself to get the requested response.",
262
+ 403: "The client does not have access rights to the content.",
263
+ 404: "The server can not find the requested resource.",
264
+ 405: "The request method is known by the server but is not supported by the target resource.",
265
+ 409: "The request conflicts with the current state of the server.",
266
+ 410: "The content has been permanently deleted from server.",
267
+ 422: "The request was well-formed but was unable to be followed due to semantic errors.",
268
+ 429: "The user has sent too many requests in a given amount of time.",
269
+ 500: "The server has encountered a situation it doesn't know how to handle.",
270
+ 501: "The request method is not supported by the server and cannot be handled.",
271
+ 502: "The server got an invalid response while working as a gateway.",
272
+ 503: "The server is not ready to handle the request.",
273
+ 504: "The server is acting as a gateway and cannot get a response in time.",
274
+ 505: "The HTTP version used in the request is not supported by the server."
275
+ };
276
+ if (code !== undefined) {
277
+ const message = statusCodes[code];
278
+ const description = descriptions[code];
279
+ if (!message) {
280
+ return {
281
+ content: [
282
+ {
283
+ type: "text",
284
+ text: `HTTP Status Code: ${code}
285
+
286
+ Status: Unknown/Custom status code
287
+
288
+ Common status codes:
289
+ ${Object.entries(statusCodes).slice(0, 10).map(([c, m]) => `• ${c}: ${m}`).join('\n')}`,
290
+ },
291
+ ],
292
+ };
293
+ }
294
+ const category = getStatusCategory(code);
295
+ return {
296
+ content: [
297
+ {
298
+ type: "text",
299
+ text: `HTTP Status Code: ${code}
300
+
301
+ Message: ${message}
302
+ Category: ${category}
303
+
304
+ Description: ${description || 'No detailed description available.'}
305
+
306
+ Usage: This status code indicates ${getUsageHint(code)}.`,
307
+ },
308
+ ],
309
+ };
310
+ }
311
+ else {
312
+ // Return overview of all status codes
313
+ const byCategory = {
314
+ '1xx (Informational)': Object.entries(statusCodes).filter(([c]) => c.startsWith('1')),
315
+ '2xx (Success)': Object.entries(statusCodes).filter(([c]) => c.startsWith('2')),
316
+ '3xx (Redirection)': Object.entries(statusCodes).filter(([c]) => c.startsWith('3')),
317
+ '4xx (Client Error)': Object.entries(statusCodes).filter(([c]) => c.startsWith('4')),
318
+ '5xx (Server Error)': Object.entries(statusCodes).filter(([c]) => c.startsWith('5'))
319
+ };
320
+ let result = 'HTTP Status Codes Reference:\n\n';
321
+ for (const [category, codes] of Object.entries(byCategory)) {
322
+ result += `${category}:\n`;
323
+ for (const [code, message] of codes) {
324
+ result += `• ${code}: ${message}\n`;
325
+ }
326
+ result += '\n';
327
+ }
328
+ return {
329
+ content: [
330
+ {
331
+ type: "text",
332
+ text: result.trim(),
333
+ },
334
+ ],
335
+ };
336
+ }
337
+ }
338
+ catch (error) {
339
+ return {
340
+ content: [
341
+ {
342
+ type: "text",
343
+ text: `Error looking up HTTP status code: ${error instanceof Error ? error.message : 'Unknown error'}`,
344
+ },
345
+ ],
346
+ };
347
+ }
348
+ });
349
+ }
350
+ function getMimeCategory(mimeType) {
351
+ if (mimeType.startsWith('text/'))
352
+ return 'Text';
353
+ if (mimeType.startsWith('image/'))
354
+ return 'Image';
355
+ if (mimeType.startsWith('audio/'))
356
+ return 'Audio';
357
+ if (mimeType.startsWith('video/'))
358
+ return 'Video';
359
+ if (mimeType.startsWith('application/'))
360
+ return 'Application';
361
+ if (mimeType.startsWith('font/'))
362
+ return 'Font';
363
+ return 'Other';
364
+ }
365
+ function getMimeDescription(mimeType) {
366
+ const descriptions = {
367
+ 'text/plain': 'Plain text file',
368
+ 'text/html': 'HTML document',
369
+ 'text/css': 'Cascading Style Sheets',
370
+ 'text/javascript': 'JavaScript code',
371
+ 'application/json': 'JSON data',
372
+ 'image/jpeg': 'JPEG image',
373
+ 'image/png': 'PNG image',
374
+ 'image/gif': 'GIF image',
375
+ 'application/pdf': 'PDF document',
376
+ 'application/zip': 'ZIP archive',
377
+ 'audio/mpeg': 'MP3 audio file',
378
+ 'video/mp4': 'MP4 video file'
379
+ };
380
+ return descriptions[mimeType] || 'No description available';
381
+ }
382
+ function getStatusCategory(code) {
383
+ if (code >= 100 && code < 200)
384
+ return '1xx Informational';
385
+ if (code >= 200 && code < 300)
386
+ return '2xx Success';
387
+ if (code >= 300 && code < 400)
388
+ return '3xx Redirection';
389
+ if (code >= 400 && code < 500)
390
+ return '4xx Client Error';
391
+ if (code >= 500 && code < 600)
392
+ return '5xx Server Error';
393
+ return 'Unknown Category';
394
+ }
395
+ function getUsageHint(code) {
396
+ if (code >= 100 && code < 200)
397
+ return 'the request is being processed';
398
+ if (code >= 200 && code < 300)
399
+ return 'the request was successful';
400
+ if (code >= 300 && code < 400)
401
+ return 'the client needs to take additional action';
402
+ if (code >= 400 && code < 500)
403
+ return 'there was an error with the client request';
404
+ if (code >= 500 && code < 600)
405
+ return 'there was an error on the server side';
406
+ return 'an unknown status';
407
+ }
package/package.json ADDED
@@ -0,0 +1,94 @@
1
+ {
2
+ "name": "it-tools-mcp",
3
+ "version": "3.0.0",
4
+ "description": "MCP server providing access to various IT tools and utilities for developers",
5
+ "type": "module",
6
+ "main": "./build/index.js",
7
+ "bin": {
8
+ "it-tools-mcp": "./build/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc && chmod +x build/index.js",
12
+ "start": "docker-compose up --build",
13
+ "start:node": "node build/index.js",
14
+ "dev": "tsc && node build/index.js",
15
+ "test": "node tests/test-server.mjs",
16
+ "test:security": "node tests/security-test.mjs",
17
+ "test:all": "npm run test && npm run test:security",
18
+ "docker:build": "docker buildx build --platform linux/amd64,linux/arm64 --provenance=true --sbom=true -t it-tools-mcp .",
19
+ "docker:build:local": "docker build -t it-tools-mcp .",
20
+ "docker:run": "docker-compose up --build",
21
+ "docker:stop": "docker-compose down",
22
+ "deploy:build": "npm run build && docker buildx build --platform linux/amd64,linux/arm64 --provenance=true --sbom=true -t it-tools-mcp:latest .",
23
+ "deploy:prod": "npm run deploy:build && docker-compose up -d"
24
+ },
25
+ "files": [
26
+ "build"
27
+ ],
28
+ "keywords": [
29
+ "mcp",
30
+ "model-context-protocol",
31
+ "it-tools",
32
+ "developer-tools",
33
+ "utilities",
34
+ "docker",
35
+ "encoding",
36
+ "hashing",
37
+ "text-processing",
38
+ "network-tools"
39
+ ],
40
+ "author": "wrenchpilot",
41
+ "license": "MIT",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/wrenchpilot/it-tools-mcp.git"
45
+ },
46
+ "homepage": "https://github.com/wrenchpilot/it-tools-mcp#readme",
47
+ "bugs": {
48
+ "url": "https://github.com/wrenchpilot/it-tools-mcp/issues"
49
+ },
50
+ "devDependencies": {
51
+ "@types/bcryptjs": "^2.4.6",
52
+ "@types/figlet": "^1.7.0",
53
+ "@types/html-to-text": "^9.0.4",
54
+ "@types/iban": "^0.0.35",
55
+ "@types/marked": "^5.0.2",
56
+ "@types/mime-types": "^3.0.1",
57
+ "@types/node": "^22.15.32",
58
+ "@types/speakeasy": "^2.0.10",
59
+ "@types/turndown": "^5.0.5",
60
+ "@types/validator": "^13.15.2",
61
+ "@types/xml-formatter": "^1.2.0",
62
+ "typescript": "^5.8.3"
63
+ },
64
+ "dependencies": {
65
+ "@iarna/toml": "^2.2.5",
66
+ "@modelcontextprotocol/sdk": "^1.12.3",
67
+ "@types/js-yaml": "^4.0.9",
68
+ "@types/papaparse": "^5.3.16",
69
+ "@types/qrcode": "^1.5.5",
70
+ "bcryptjs": "^3.0.2",
71
+ "bip39": "^3.1.0",
72
+ "color": "^5.0.0",
73
+ "cron-parser": "^5.3.0",
74
+ "diff": "^8.0.2",
75
+ "emoji-js": "^3.8.1",
76
+ "figlet": "^1.8.1",
77
+ "html-to-text": "^9.0.5",
78
+ "iban": "^0.0.14",
79
+ "js-yaml": "^4.1.0",
80
+ "libphonenumber-js": "^1.12.9",
81
+ "marked": "^15.0.12",
82
+ "mathjs": "^14.5.2",
83
+ "mime-types": "^3.0.1",
84
+ "papaparse": "^5.5.3",
85
+ "qrcode": "^1.5.4",
86
+ "speakeasy": "^2.0.0",
87
+ "sql-formatter": "^15.6.5",
88
+ "turndown": "^7.2.0",
89
+ "ulid": "^3.0.1",
90
+ "validator": "^13.15.15",
91
+ "xml-formatter": "^3.6.6",
92
+ "zod": "^3.25.67"
93
+ }
94
+ }