revxl-devtools 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 +84 -0
- package/checkout/index.html +195 -0
- package/dist/auth.d.ts +3 -0
- package/dist/auth.js +77 -0
- package/dist/codegen/cron-codegen.d.ts +1 -0
- package/dist/codegen/cron-codegen.js +56 -0
- package/dist/codegen/regex-codegen.d.ts +1 -0
- package/dist/codegen/regex-codegen.js +125 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +100 -0
- package/dist/registry.d.ts +10 -0
- package/dist/registry.js +13 -0
- package/dist/tools/base64.d.ts +1 -0
- package/dist/tools/base64.js +29 -0
- package/dist/tools/batch.d.ts +1 -0
- package/dist/tools/batch.js +56 -0
- package/dist/tools/chmod.d.ts +1 -0
- package/dist/tools/chmod.js +115 -0
- package/dist/tools/cron.d.ts +1 -0
- package/dist/tools/cron.js +311 -0
- package/dist/tools/hash.d.ts +1 -0
- package/dist/tools/hash.js +25 -0
- package/dist/tools/http-status.d.ts +1 -0
- package/dist/tools/http-status.js +59 -0
- package/dist/tools/json-diff.d.ts +1 -0
- package/dist/tools/json-diff.js +131 -0
- package/dist/tools/json-format.d.ts +1 -0
- package/dist/tools/json-format.js +38 -0
- package/dist/tools/json-query.d.ts +1 -0
- package/dist/tools/json-query.js +114 -0
- package/dist/tools/jwt.d.ts +1 -0
- package/dist/tools/jwt.js +177 -0
- package/dist/tools/regex.d.ts +1 -0
- package/dist/tools/regex.js +116 -0
- package/dist/tools/secrets-scan.d.ts +1 -0
- package/dist/tools/secrets-scan.js +173 -0
- package/dist/tools/sql-format.d.ts +1 -0
- package/dist/tools/sql-format.js +157 -0
- package/dist/tools/timestamp.d.ts +1 -0
- package/dist/tools/timestamp.js +72 -0
- package/dist/tools/url-encode.d.ts +1 -0
- package/dist/tools/url-encode.js +26 -0
- package/dist/tools/uuid.d.ts +1 -0
- package/dist/tools/uuid.js +24 -0
- package/dist/tools/yaml-convert.d.ts +1 -0
- package/dist/tools/yaml-convert.js +371 -0
- package/package.json +29 -0
- package/src/auth.ts +99 -0
- package/src/codegen/cron-codegen.ts +66 -0
- package/src/codegen/regex-codegen.ts +132 -0
- package/src/index.ts +134 -0
- package/src/registry.ts +25 -0
- package/src/tools/base64.ts +32 -0
- package/src/tools/batch.ts +69 -0
- package/src/tools/chmod.ts +133 -0
- package/src/tools/cron.ts +365 -0
- package/src/tools/hash.ts +26 -0
- package/src/tools/http-status.ts +63 -0
- package/src/tools/json-diff.ts +153 -0
- package/src/tools/json-format.ts +43 -0
- package/src/tools/json-query.ts +126 -0
- package/src/tools/jwt.ts +193 -0
- package/src/tools/regex.ts +131 -0
- package/src/tools/secrets-scan.ts +212 -0
- package/src/tools/sql-format.ts +178 -0
- package/src/tools/timestamp.ts +74 -0
- package/src/tools/url-encode.ts +29 -0
- package/src/tools/uuid.ts +25 -0
- package/src/tools/yaml-convert.ts +383 -0
- package/tsconfig.json +14 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
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 {
|
|
10
|
+
checkProAccess,
|
|
11
|
+
getTrialUsesRemaining,
|
|
12
|
+
incrementTrialUse,
|
|
13
|
+
} from "./auth.js";
|
|
14
|
+
import {
|
|
15
|
+
registerTool,
|
|
16
|
+
getToolByName,
|
|
17
|
+
getAllTools,
|
|
18
|
+
} from "./registry.js";
|
|
19
|
+
import type { ToolDefinition } from "./registry.js";
|
|
20
|
+
|
|
21
|
+
// Re-export for tool files that import from index
|
|
22
|
+
export { registerTool, getToolByName, getAllTools };
|
|
23
|
+
export type { ToolDefinition };
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// MCP Server
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
const PURCHASE_URL = "https://revxl-devtools.vercel.app";
|
|
30
|
+
|
|
31
|
+
const server = new Server(
|
|
32
|
+
{ name: "@revxl/devtools", version: "1.0.0" },
|
|
33
|
+
{ capabilities: { tools: {} } }
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// --- ListTools ------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
39
|
+
const toolList = getAllTools().map((t) => ({
|
|
40
|
+
name: t.name,
|
|
41
|
+
description: t.pro
|
|
42
|
+
? `${t.description} [PRO - 3 free trials]`
|
|
43
|
+
: t.description,
|
|
44
|
+
inputSchema: t.inputSchema,
|
|
45
|
+
}));
|
|
46
|
+
return { tools: toolList };
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// --- CallTool -------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
52
|
+
const { name, arguments: args } = request.params;
|
|
53
|
+
|
|
54
|
+
const tool = getToolByName(name);
|
|
55
|
+
if (!tool) {
|
|
56
|
+
return {
|
|
57
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
58
|
+
isError: true,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Pro gate
|
|
63
|
+
if (tool.pro) {
|
|
64
|
+
const isPro = await checkProAccess();
|
|
65
|
+
if (!isPro) {
|
|
66
|
+
const remaining = getTrialUsesRemaining(name);
|
|
67
|
+
if (remaining <= 0) {
|
|
68
|
+
return {
|
|
69
|
+
content: [
|
|
70
|
+
{
|
|
71
|
+
type: "text",
|
|
72
|
+
text: `⚡ ${name} is a Pro tool and you've used all 3 free trials.\n\nUpgrade for $7 one-time at ${PURCHASE_URL} to unlock unlimited access to all Pro tools.`,
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
isError: false,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
incrementTrialUse(name);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const result = await tool.handler((args ?? {}) as Record<string, unknown>);
|
|
84
|
+
const text =
|
|
85
|
+
typeof result === "string" ? result : JSON.stringify(result, null, 2);
|
|
86
|
+
return { content: [{ type: "text", text }] };
|
|
87
|
+
} catch (err: unknown) {
|
|
88
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
89
|
+
return {
|
|
90
|
+
content: [{ type: "text", text: `Error in ${name}: ${message}` }],
|
|
91
|
+
isError: true,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Tool imports
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
// Free tools
|
|
101
|
+
import "./tools/json-format.js";
|
|
102
|
+
import "./tools/base64.js";
|
|
103
|
+
import "./tools/url-encode.js";
|
|
104
|
+
import "./tools/uuid.js";
|
|
105
|
+
import "./tools/hash.js";
|
|
106
|
+
import "./tools/timestamp.js";
|
|
107
|
+
import "./tools/http-status.js";
|
|
108
|
+
|
|
109
|
+
// Pro tools
|
|
110
|
+
import "./tools/jwt.js";
|
|
111
|
+
import "./tools/regex.js";
|
|
112
|
+
import "./tools/cron.js";
|
|
113
|
+
import "./tools/json-diff.js";
|
|
114
|
+
import "./tools/json-query.js";
|
|
115
|
+
import "./tools/batch.js";
|
|
116
|
+
import "./tools/sql-format.js";
|
|
117
|
+
import "./tools/yaml-convert.js";
|
|
118
|
+
import "./tools/chmod.js";
|
|
119
|
+
import "./tools/secrets-scan.js";
|
|
120
|
+
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Start
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
async function main(): Promise<void> {
|
|
126
|
+
const transport = new StdioServerTransport();
|
|
127
|
+
await server.connect(transport);
|
|
128
|
+
console.error("@revxl/devtools MCP server running on stdio");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
main().catch((err) => {
|
|
132
|
+
console.error("Fatal error:", err);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
});
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Tool registry — separated to avoid circular import issues with ESM hoisting
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
export interface ToolDefinition {
|
|
6
|
+
name: string;
|
|
7
|
+
description: string;
|
|
8
|
+
pro: boolean;
|
|
9
|
+
inputSchema: Record<string, unknown>;
|
|
10
|
+
handler: (args: Record<string, unknown>) => Promise<unknown>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const tools: Map<string, ToolDefinition> = new Map();
|
|
14
|
+
|
|
15
|
+
export function registerTool(tool: ToolDefinition): void {
|
|
16
|
+
tools.set(tool.name, tool);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getToolByName(name: string): ToolDefinition | undefined {
|
|
20
|
+
return tools.get(name);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getAllTools(): ToolDefinition[] {
|
|
24
|
+
return Array.from(tools.values());
|
|
25
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { registerTool } from "../registry.js";
|
|
2
|
+
|
|
3
|
+
registerTool({
|
|
4
|
+
name: "base64",
|
|
5
|
+
description: "Encode or decode Base64 strings",
|
|
6
|
+
pro: false,
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: "object",
|
|
9
|
+
properties: {
|
|
10
|
+
text: { type: "string", description: "Text to encode or Base64 string to decode" },
|
|
11
|
+
action: {
|
|
12
|
+
type: "string",
|
|
13
|
+
enum: ["encode", "decode"],
|
|
14
|
+
description: "encode or decode",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
required: ["text", "action"],
|
|
18
|
+
},
|
|
19
|
+
handler: async (args) => {
|
|
20
|
+
const text = args.text as string;
|
|
21
|
+
const action = args.action as string;
|
|
22
|
+
|
|
23
|
+
if (action === "encode") {
|
|
24
|
+
return Buffer.from(text, "utf-8").toString("base64");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Handle URL-safe Base64 variant
|
|
28
|
+
const normalized = text.replace(/-/g, "+").replace(/_/g, "/");
|
|
29
|
+
const decoded = Buffer.from(normalized, "base64").toString("utf-8");
|
|
30
|
+
return decoded;
|
|
31
|
+
},
|
|
32
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { registerTool, getToolByName } from "../registry.js";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Batch runner — execute a free tool across multiple inputs
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
const MAX_ITEMS = 500;
|
|
8
|
+
|
|
9
|
+
registerTool({
|
|
10
|
+
name: "batch",
|
|
11
|
+
description:
|
|
12
|
+
"Run any free tool across multiple inputs in one call — up to 500 items. Only works with free (non-Pro) tools.",
|
|
13
|
+
pro: true,
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: {
|
|
17
|
+
tool: {
|
|
18
|
+
type: "string",
|
|
19
|
+
description: "Name of the tool to run (must be a free tool)",
|
|
20
|
+
},
|
|
21
|
+
items: {
|
|
22
|
+
type: "array",
|
|
23
|
+
items: { type: "object" },
|
|
24
|
+
description: "Array of argument objects — each is passed to the tool's handler (max 500)",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
required: ["tool", "items"],
|
|
28
|
+
},
|
|
29
|
+
handler: async (args) => {
|
|
30
|
+
const toolName = args.tool as string;
|
|
31
|
+
const items = args.items as Record<string, unknown>[];
|
|
32
|
+
|
|
33
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
34
|
+
throw new Error("items must be a non-empty array");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (items.length > MAX_ITEMS) {
|
|
38
|
+
throw new Error(`Maximum ${MAX_ITEMS} items per batch (got ${items.length})`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const tool = getToolByName(toolName);
|
|
42
|
+
if (!tool) {
|
|
43
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (tool.pro) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Batch only works with free tools. "${toolName}" is a Pro tool.`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const results: string[] = [];
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < items.length; i++) {
|
|
55
|
+
try {
|
|
56
|
+
const result = await tool.handler(items[i]);
|
|
57
|
+
const text =
|
|
58
|
+
typeof result === "string" ? result : JSON.stringify(result, null, 2);
|
|
59
|
+
results.push(`[${i}] ${text}`);
|
|
60
|
+
} catch (err: unknown) {
|
|
61
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
62
|
+
results.push(`[${i}] ERROR: ${message}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const header = `Batch ${toolName}: ${items.length} items`;
|
|
67
|
+
return `${header}\n${"=".repeat(header.length)}\n\n${results.join("\n\n")}`;
|
|
68
|
+
},
|
|
69
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { registerTool } from "../registry.js";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Chmod converter — numeric ↔ symbolic with human-readable explanation
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
const PERM_MAP: Record<number, string> = {
|
|
8
|
+
0: "---",
|
|
9
|
+
1: "--x",
|
|
10
|
+
2: "-w-",
|
|
11
|
+
3: "-wx",
|
|
12
|
+
4: "r--",
|
|
13
|
+
5: "r-x",
|
|
14
|
+
6: "rw-",
|
|
15
|
+
7: "rwx",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const ROLE_NAMES = ["Owner", "Group", "Others"] as const;
|
|
19
|
+
|
|
20
|
+
const PERM_LABELS: Record<string, string> = {
|
|
21
|
+
r: "read",
|
|
22
|
+
w: "write",
|
|
23
|
+
x: "execute",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function numericToSymbolic(mode: string): string {
|
|
27
|
+
const digits = mode.split("").map(Number);
|
|
28
|
+
return digits.map((d) => PERM_MAP[d]).join("");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function symbolicToNumeric(mode: string): string {
|
|
32
|
+
// Expect 9-char symbolic like "rwxr-xr-x"
|
|
33
|
+
const clean = mode.replace(/^-/, ""); // strip leading - if present (like -rwxr-xr-x)
|
|
34
|
+
const chars = clean.length >= 9 ? clean.slice(0, 9) : clean;
|
|
35
|
+
let result = "";
|
|
36
|
+
for (let i = 0; i < 3; i++) {
|
|
37
|
+
const group = chars.slice(i * 3, i * 3 + 3);
|
|
38
|
+
let val = 0;
|
|
39
|
+
if (group[0] === "r") val += 4;
|
|
40
|
+
if (group[1] === "w") val += 2;
|
|
41
|
+
if (group[2] === "x") val += 1;
|
|
42
|
+
result += val;
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function describePermissions(numericMode: string): string[] {
|
|
48
|
+
const digits = numericMode.split("").map(Number);
|
|
49
|
+
const descriptions: string[] = [];
|
|
50
|
+
|
|
51
|
+
for (let i = 0; i < 3; i++) {
|
|
52
|
+
const symbolic = PERM_MAP[digits[i]];
|
|
53
|
+
const perms: string[] = [];
|
|
54
|
+
for (const ch of symbolic) {
|
|
55
|
+
if (ch !== "-" && PERM_LABELS[ch]) {
|
|
56
|
+
perms.push(PERM_LABELS[ch]);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const permStr = perms.length > 0 ? perms.join(", ") : "none";
|
|
60
|
+
descriptions.push(`${ROLE_NAMES[i]}: ${permStr}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return descriptions;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isNumericMode(mode: string): boolean {
|
|
67
|
+
return /^[0-7]{3,4}$/.test(mode);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isSymbolicMode(mode: string): boolean {
|
|
71
|
+
// Accept rwxr-xr-x or -rwxr-xr-x
|
|
72
|
+
return /^-?[rwx-]{9}$/.test(mode);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
registerTool({
|
|
76
|
+
name: "chmod",
|
|
77
|
+
description:
|
|
78
|
+
"Convert between numeric (755) and symbolic (rwxr-xr-x) chmod permissions with explanation",
|
|
79
|
+
pro: true,
|
|
80
|
+
inputSchema: {
|
|
81
|
+
type: "object",
|
|
82
|
+
properties: {
|
|
83
|
+
mode: {
|
|
84
|
+
type: "string",
|
|
85
|
+
description:
|
|
86
|
+
'Permission mode — numeric (e.g. "755") or symbolic (e.g. "rwxr-xr-x")',
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
required: ["mode"],
|
|
90
|
+
},
|
|
91
|
+
handler: async (args) => {
|
|
92
|
+
const mode = (args.mode as string).trim();
|
|
93
|
+
|
|
94
|
+
if (!mode) throw new Error("mode is required");
|
|
95
|
+
|
|
96
|
+
if (isNumericMode(mode)) {
|
|
97
|
+
// Take last 3 digits (ignore leading 0 in 0755)
|
|
98
|
+
const digits = mode.slice(-3);
|
|
99
|
+
const symbolic = numericToSymbolic(digits);
|
|
100
|
+
const explanation = describePermissions(digits);
|
|
101
|
+
|
|
102
|
+
return [
|
|
103
|
+
"=== Chmod: Numeric → Symbolic ===",
|
|
104
|
+
"",
|
|
105
|
+
`Numeric: ${digits}`,
|
|
106
|
+
`Symbolic: ${symbolic}`,
|
|
107
|
+
"",
|
|
108
|
+
"--- Permissions ---",
|
|
109
|
+
...explanation.map((e) => ` ${e}`),
|
|
110
|
+
].join("\n");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (isSymbolicMode(mode)) {
|
|
114
|
+
const clean = mode.startsWith("-") ? mode.slice(1) : mode;
|
|
115
|
+
const numeric = symbolicToNumeric(clean);
|
|
116
|
+
const explanation = describePermissions(numeric);
|
|
117
|
+
|
|
118
|
+
return [
|
|
119
|
+
"=== Chmod: Symbolic → Numeric ===",
|
|
120
|
+
"",
|
|
121
|
+
`Symbolic: ${clean}`,
|
|
122
|
+
`Numeric: ${numeric}`,
|
|
123
|
+
"",
|
|
124
|
+
"--- Permissions ---",
|
|
125
|
+
...explanation.map((e) => ` ${e}`),
|
|
126
|
+
].join("\n");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
throw new Error(
|
|
130
|
+
`Invalid mode: "${mode}". Use numeric (e.g. "755") or symbolic (e.g. "rwxr-xr-x").`,
|
|
131
|
+
);
|
|
132
|
+
},
|
|
133
|
+
});
|