@mrfentmen/awk-mcp 0.0.0-stage → 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 CHANGED
@@ -1,3 +1,53 @@
1
- # Temporary Holding Version
1
+ # Awk MCP
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ AWK is a missing language in the MCP ecosystem. This server gives agents real text-processing tools instead of a stub.
4
+
5
+ Keyless. No API key. No signup. Requires the built-in `awk` command.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npx -y @mrfentmen/awk-mcp
11
+ ```
12
+
13
+ Or in an MCP client:
14
+
15
+ ```json
16
+ {
17
+ "mcpServers": {
18
+ "awk-mcp": {
19
+ "command": "npx",
20
+ "args": ["-y", "@mrfentmen/awk-mcp"]
21
+ }
22
+ }
23
+ }
24
+ ```
25
+
26
+ ## Tools
27
+
28
+ | Tool | What it does |
29
+ |---|---|
30
+ | `run_program` | Run an inline AWK program. |
31
+ | `run_script_file` | Run an AWK program from a file. |
32
+ | `check_program` | Parse an AWK program and report syntax errors. |
33
+ | `extract_columns` | Print selected columns. |
34
+ | `filter_rows` | Keep rows where an AWK condition is true. |
35
+ | `sum_column` | Sum one numeric column. |
36
+ | `average_column` | Average one numeric column. |
37
+ | `unique_column` | Print distinct values in first-seen order. |
38
+ | `sort_by_column` | Sort lines by one column. |
39
+ | `join_files` | Join two files on key columns. |
40
+ | `count_rows` | Count input rows. |
41
+ | `print_version` | Show the local AWK version. |
42
+
43
+ ## Safety
44
+
45
+ This server executes the local `awk` command. Treat scripts and targets as trusted input. `transform_file` refuses to overwrite its input path.
46
+
47
+ ## Run locally
48
+
49
+ ```bash
50
+ npm install
51
+ npm run build
52
+ node dist/index.js
53
+ ```
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { createServer } from "./server.js";
4
+ async function main() {
5
+ const server = createServer();
6
+ const transport = new StdioServerTransport();
7
+ await server.connect(transport);
8
+ console.error(`${process.env.npm_package_name ?? "language-mcp"} running on stdio`);
9
+ }
10
+ main().catch((error) => {
11
+ console.error("Fatal error:", error);
12
+ process.exit(1);
13
+ });
package/dist/run.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ export declare class ToolError extends Error {
2
+ }
3
+ export interface RunResult {
4
+ stdout: string;
5
+ stderr: string;
6
+ code: number;
7
+ }
8
+ export interface RunOptions {
9
+ input?: string;
10
+ timeoutMs?: number;
11
+ cwd?: string;
12
+ }
13
+ /** Run a command without a shell and capture both streams. */
14
+ export declare function run(command: string, args: string[], options?: RunOptions): Promise<RunResult>;
15
+ export declare function runChecked(command: string, args: string[], options?: RunOptions): Promise<RunResult>;
16
+ export declare function requireCommand(command: string): Promise<void>;
17
+ export declare function text(value: string): {
18
+ content: {
19
+ type: "text";
20
+ text: string;
21
+ }[];
22
+ };
23
+ export declare function fail(value: string): {
24
+ content: {
25
+ type: "text";
26
+ text: string;
27
+ }[];
28
+ isError: boolean;
29
+ };
30
+ export declare function errorMessage(error: unknown): string;
31
+ export declare function clean(value: string): string;
package/dist/run.js ADDED
@@ -0,0 +1,71 @@
1
+ import { spawn } from "node:child_process";
2
+ export class ToolError extends Error {
3
+ }
4
+ /** Run a command without a shell and capture both streams. */
5
+ export function run(command, args, options = {}) {
6
+ return new Promise((resolve, reject) => {
7
+ const child = spawn(command, args, {
8
+ cwd: options.cwd,
9
+ stdio: ["pipe", "pipe", "pipe"],
10
+ });
11
+ let stdout = "";
12
+ let stderr = "";
13
+ let settled = false;
14
+ const timeoutMs = options.timeoutMs ?? 30_000;
15
+ const timer = setTimeout(() => {
16
+ if (settled)
17
+ return;
18
+ settled = true;
19
+ child.kill("SIGKILL");
20
+ reject(new ToolError(`${command} timed out after ${timeoutMs}ms`));
21
+ }, timeoutMs);
22
+ child.on("error", (error) => {
23
+ if (settled)
24
+ return;
25
+ settled = true;
26
+ clearTimeout(timer);
27
+ reject(new ToolError(`${command} could not start: ${error.message}`));
28
+ });
29
+ child.stdout.on("data", (chunk) => {
30
+ stdout += chunk.toString();
31
+ });
32
+ child.stderr.on("data", (chunk) => {
33
+ stderr += chunk.toString();
34
+ });
35
+ child.stdin.on("error", () => { });
36
+ child.on("close", (code) => {
37
+ if (settled)
38
+ return;
39
+ settled = true;
40
+ clearTimeout(timer);
41
+ resolve({ stdout, stderr, code: code ?? 0 });
42
+ });
43
+ if (options.input !== undefined)
44
+ child.stdin.write(options.input);
45
+ child.stdin.end();
46
+ });
47
+ }
48
+ export async function runChecked(command, args, options = {}) {
49
+ const result = await run(command, args, options);
50
+ if (result.code !== 0) {
51
+ throw new ToolError((result.stderr || result.stdout || `${command} exited with code ${result.code}`).trim());
52
+ }
53
+ return result;
54
+ }
55
+ export async function requireCommand(command) {
56
+ const result = await run("which", [command], { timeoutMs: 5_000 });
57
+ if (result.code !== 0)
58
+ throw new ToolError(`${command} is not installed or not on PATH`);
59
+ }
60
+ export function text(value) {
61
+ return { content: [{ type: "text", text: value }] };
62
+ }
63
+ export function fail(value) {
64
+ return { content: [{ type: "text", text: value }], isError: true };
65
+ }
66
+ export function errorMessage(error) {
67
+ return error instanceof Error ? error.message : String(error);
68
+ }
69
+ export function clean(value) {
70
+ return value.replace(/\s+/g, " ").trim();
71
+ }
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function createServer(): McpServer;
package/dist/server.js ADDED
@@ -0,0 +1,226 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { z } from "zod";
3
+ import { errorMessage, fail, run, runChecked, text, ToolError, } from "./run.js";
4
+ const MAX_INPUT = 2_000_000;
5
+ const input = z.string().max(MAX_INPUT).default("");
6
+ const timeout = z.number().int().min(1_000).max(120_000).default(30_000);
7
+ const delimiter = z.string().min(1).max(4).optional();
8
+ const file = z.string().min(1).max(2_000).optional();
9
+ const awkLit = (value) => JSON.stringify(value);
10
+ function fsArgs(separator) {
11
+ return separator ? ["-F", separator] : [];
12
+ }
13
+ function sourceArgs(separator, outputSeparator) {
14
+ const parts = [];
15
+ if (separator)
16
+ parts.push(`FS=${awkLit(separator)}`);
17
+ if (outputSeparator)
18
+ parts.push(`OFS=${awkLit(outputSeparator)}`);
19
+ return parts.length ? `BEGIN{${parts.join(";")}} ` : "";
20
+ }
21
+ async function awk(args, stdin, timeoutMs) {
22
+ const result = await run("awk", args, { input: stdin, timeoutMs });
23
+ if (result.code !== 0) {
24
+ throw new ToolError(result.stderr.trim() || `awk exited with code ${result.code}`);
25
+ }
26
+ return result.stdout;
27
+ }
28
+ function inputOrFile(stdin, path) {
29
+ if (path && stdin.length > 0)
30
+ throw new ToolError("Use input or file, not both");
31
+ return { stdin: path ? "" : stdin, extra: path ? [path] : [] };
32
+ }
33
+ export function createServer() {
34
+ const server = new McpServer({ name: "awk-mcp", version: "1.0.0" });
35
+ server.tool("run_program", "Run an inline AWK program. Use input text or a file, never both.", {
36
+ program: z.string().min(1).max(100_000),
37
+ input,
38
+ file,
39
+ fieldSeparator: delimiter,
40
+ timeoutMs: timeout,
41
+ }, async (a) => {
42
+ try {
43
+ const source = inputOrFile(a.input, a.file);
44
+ const output = await awk([...fsArgs(a.fieldSeparator), a.program, ...source.extra], source.stdin, a.timeoutMs);
45
+ return text(output || "(no output)");
46
+ }
47
+ catch (error) {
48
+ return fail(errorMessage(error));
49
+ }
50
+ });
51
+ server.tool("run_script_file", "Run an AWK program from a .awk file. Use input text or a file, never both.", {
52
+ scriptPath: z.string().min(1).max(2_000),
53
+ input,
54
+ file,
55
+ fieldSeparator: delimiter,
56
+ timeoutMs: timeout,
57
+ }, async (a) => {
58
+ try {
59
+ const source = inputOrFile(a.input, a.file);
60
+ const output = await awk([...fsArgs(a.fieldSeparator), "-f", a.scriptPath, ...source.extra], source.stdin, a.timeoutMs);
61
+ return text(output || "(no output)");
62
+ }
63
+ catch (error) {
64
+ return fail(errorMessage(error));
65
+ }
66
+ });
67
+ server.tool("check_program", "Parse an AWK program without real input and return its syntax errors.", {
68
+ program: z.string().min(1).max(100_000),
69
+ fieldSeparator: delimiter,
70
+ }, async (a) => {
71
+ try {
72
+ await awk([...fsArgs(a.fieldSeparator), a.program, "/dev/null"], "", 30_000);
73
+ return text("AWK syntax OK");
74
+ }
75
+ catch (error) {
76
+ return fail(errorMessage(error));
77
+ }
78
+ });
79
+ server.tool("extract_columns", "Print selected 1-based columns from delimited input.", {
80
+ input: z.string().min(1).max(MAX_INPUT),
81
+ columns: z.array(z.number().int().min(1).max(100)).min(1).max(30),
82
+ fieldSeparator: delimiter,
83
+ outputSeparator: delimiter,
84
+ }, async (a) => {
85
+ try {
86
+ const picks = a.columns.map((column) => `$${column}`).join(", ");
87
+ const program = `${sourceArgs(a.fieldSeparator, a.outputSeparator)}{print ${picks}}`;
88
+ return text(await awk([...fsArgs(a.fieldSeparator), program], a.input, 30_000));
89
+ }
90
+ catch (error) {
91
+ return fail(errorMessage(error));
92
+ }
93
+ });
94
+ server.tool("filter_rows", "Keep rows where an AWK expression is true, e.g. $3 > 10 or $1 == \"ok\".", {
95
+ input: z.string().min(1).max(MAX_INPUT),
96
+ condition: z.string().min(1).max(20_000),
97
+ fieldSeparator: delimiter,
98
+ outputSeparator: delimiter,
99
+ }, async (a) => {
100
+ try {
101
+ const program = `${sourceArgs(a.fieldSeparator, a.outputSeparator)}{if (${a.condition}) print}`;
102
+ return text(await awk([...fsArgs(a.fieldSeparator), program], a.input, 30_000));
103
+ }
104
+ catch (error) {
105
+ return fail(errorMessage(error));
106
+ }
107
+ });
108
+ server.tool("sum_column", "Sum one numeric column.", {
109
+ input: z.string().min(1).max(MAX_INPUT),
110
+ column: z.number().int().min(1).max(100),
111
+ fieldSeparator: delimiter,
112
+ }, async (a) => {
113
+ try {
114
+ const program = `${sourceArgs(a.fieldSeparator, undefined)}{s += $${a.column}} END{printf "%.6f\\n", s}`;
115
+ return text(await awk([...fsArgs(a.fieldSeparator), program], a.input, 30_000));
116
+ }
117
+ catch (error) {
118
+ return fail(errorMessage(error));
119
+ }
120
+ });
121
+ server.tool("average_column", "Average one numeric column. Empty input returns 0.", {
122
+ input: z.string().min(1).max(MAX_INPUT),
123
+ column: z.number().int().min(1).max(100),
124
+ fieldSeparator: delimiter,
125
+ }, async (a) => {
126
+ try {
127
+ const program = `${sourceArgs(a.fieldSeparator, undefined)}{s += $${a.column}; c++} END{if (c) printf "%.6f\\n", s / c; else print "0"}`;
128
+ return text(await awk([...fsArgs(a.fieldSeparator), program], a.input, 30_000));
129
+ }
130
+ catch (error) {
131
+ return fail(errorMessage(error));
132
+ }
133
+ });
134
+ server.tool("unique_column", "Print each distinct value in a column once, in first-seen order.", {
135
+ input: z.string().min(1).max(MAX_INPUT),
136
+ column: z.number().int().min(1).max(100),
137
+ fieldSeparator: delimiter,
138
+ }, async (a) => {
139
+ try {
140
+ const program = `${sourceArgs(a.fieldSeparator, undefined)}!seen[$${a.column}]++ {print $${a.column}}`;
141
+ return text(await awk([...fsArgs(a.fieldSeparator), program], a.input, 30_000));
142
+ }
143
+ catch (error) {
144
+ return fail(errorMessage(error));
145
+ }
146
+ });
147
+ server.tool("sort_by_column", "Sort lines by one 1-based column, optionally numeric and descending.", {
148
+ input: z.string().min(1).max(MAX_INPUT),
149
+ column: z.number().int().min(1).max(100),
150
+ fieldSeparator: delimiter,
151
+ numeric: z.boolean().default(false),
152
+ reverse: z.boolean().default(false),
153
+ }, async (a) => {
154
+ try {
155
+ const args = [];
156
+ if (a.fieldSeparator)
157
+ args.push("-t", a.fieldSeparator);
158
+ args.push("-k", `${a.column},${a.column}`);
159
+ if (a.numeric)
160
+ args.push("-n");
161
+ if (a.reverse)
162
+ args.push("-r");
163
+ const result = await runChecked("sort", args, { input: a.input, timeoutMs: 30_000 });
164
+ return text(result.stdout);
165
+ }
166
+ catch (error) {
167
+ return fail(errorMessage(error));
168
+ }
169
+ });
170
+ server.tool("join_files", "Join two delimited files on numeric key columns.", {
171
+ left: z.string().min(1).max(2_000),
172
+ right: z.string().min(1).max(2_000),
173
+ leftKey: z.number().int().min(1).max(100),
174
+ rightKey: z.number().int().min(1).max(100),
175
+ fieldSeparator: delimiter,
176
+ outputSeparator: delimiter,
177
+ }, async (a) => {
178
+ try {
179
+ const program = `${sourceArgs(a.fieldSeparator, a.outputSeparator)}` +
180
+ `NR == FNR {key = $${a.leftKey}; rows[key] = $0; next} ` +
181
+ `{key = $${a.rightKey}; if (key in rows) print rows[key], $0}`;
182
+ const result = await run("awk", [...fsArgs(a.fieldSeparator), program, a.left, a.right], {
183
+ timeoutMs: 30_000,
184
+ });
185
+ if (result.code !== 0) {
186
+ throw new ToolError(result.stderr.trim() || `awk exited with code ${result.code}`);
187
+ }
188
+ return text(result.stdout || "(no matching keys)");
189
+ }
190
+ catch (error) {
191
+ return fail(errorMessage(error));
192
+ }
193
+ });
194
+ server.tool("count_rows", "Count input rows.", {
195
+ input: z.string().min(1).max(MAX_INPUT),
196
+ fieldSeparator: delimiter,
197
+ }, async (a) => {
198
+ try {
199
+ const result = await run("awk", [...fsArgs(a.fieldSeparator), "END{print NR}"], {
200
+ input: a.input,
201
+ timeoutMs: 30_000,
202
+ });
203
+ if (result.code !== 0)
204
+ throw new ToolError(result.stderr.trim() || "awk failed");
205
+ return text(result.stdout.trim());
206
+ }
207
+ catch (error) {
208
+ return fail(errorMessage(error));
209
+ }
210
+ });
211
+ server.tool("print_version", "Show the local AWK implementation and version when available.", {}, async () => {
212
+ try {
213
+ let result = await run("awk", ["--version"], { timeoutMs: 10_000 });
214
+ if (result.code !== 0)
215
+ result = await run("awk", ["-W", "version"], { timeoutMs: 10_000 });
216
+ if (result.code === 0)
217
+ return text((result.stdout || result.stderr).trim());
218
+ const path = await run("which", ["awk"], { timeoutMs: 5_000 });
219
+ return text(`awk is available at ${path.stdout.trim() || "unknown path"}; this build has no version flag`);
220
+ }
221
+ catch (error) {
222
+ return fail(errorMessage(error));
223
+ }
224
+ });
225
+ return server;
226
+ }
package/package.json CHANGED
@@ -1,6 +1,44 @@
1
1
  {
2
2
  "name": "@mrfentmen/awk-mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "Awk MCP: run programs, filter rows, aggregate columns and join delimited files with 12 real tools.",
5
+ "type": "module",
6
+ "mcpName": "io.github.mrfentmen/awk-mcp",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/mrfentmen/awesome-servers.git"
10
+ },
11
+ "bin": {
12
+ "awk-mcp": "./dist/index.js"
13
+ },
14
+ "main": "./dist/index.js",
15
+ "files": [
16
+ "dist",
17
+ "server.json",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsc -p tsconfig.json",
22
+ "start": "node dist/index.js",
23
+ "test": "npm run build && node --test tests/tools.test.mjs"
24
+ },
25
+ "keywords": [
26
+ "mcp",
27
+ "model-context-protocol",
28
+ "awk",
29
+ "text-processing",
30
+ "cli"
31
+ ],
32
+ "license": "MIT",
33
+ "dependencies": {
34
+ "@modelcontextprotocol/sdk": "^1.0.4",
35
+ "zod": "^3.23.8"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^22.0.0",
39
+ "typescript": "^5.6.0"
40
+ },
41
+ "engines": {
42
+ "node": ">=20"
43
+ }
44
+ }
package/server.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.mrfentmen/awk-mcp",
4
+ "description": "Awk MCP: run programs, filter rows, aggregate columns and join delimited files with 12 real tools.",
5
+ "repository": {
6
+ "url": "https://github.com/mrfentmen/awesome-servers/tree/main/mcp/servers/awk-mcp",
7
+ "source": "github"
8
+ },
9
+ "version": "1.0.0",
10
+ "packages": [
11
+ {
12
+ "registryType": "npm",
13
+ "identifier": "@mrfentmen/awk-mcp",
14
+ "version": "1.0.0",
15
+ "transport": {
16
+ "type": "stdio"
17
+ }
18
+ }
19
+ ]
20
+ }