@mrfentmen/openedge-abl-mcp 1.0.0 → 1.0.1
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 +18 -23
- package/dist/core.d.ts +2 -2
- package/dist/core.js +214 -573
- package/dist/format.d.ts +2 -0
- package/dist/format.js +105 -0
- package/dist/index.js +2 -2
- package/dist/run.d.ts +4 -15
- package/dist/run.js +8 -49
- package/dist/server.d.ts +1 -0
- package/dist/server.js +5 -4
- package/dist/types.d.ts +22 -38
- package/package.json +7 -5
- package/server.json +4 -4
- package/dist/config.d.ts +0 -2
- package/dist/config.js +0 -47
package/dist/format.d.ts
ADDED
package/dist/format.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
function stripComments(text) {
|
|
2
|
+
return text.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
3
|
+
}
|
|
4
|
+
function parse(text) {
|
|
5
|
+
const issues = [];
|
|
6
|
+
const records = [];
|
|
7
|
+
const source = stripComments(text);
|
|
8
|
+
const variables = new Set();
|
|
9
|
+
const tables = new Set();
|
|
10
|
+
let openers = 0;
|
|
11
|
+
let closers = 0;
|
|
12
|
+
source.split(/\r?\n/).forEach((raw, index) => {
|
|
13
|
+
const number = index + 1;
|
|
14
|
+
const line = raw.replace(/\/\/.*$/, "").replace(/^\s*\*.*$/, "").trim();
|
|
15
|
+
if (line.length === 0)
|
|
16
|
+
return;
|
|
17
|
+
const variable = /^DEFINE\s+(?:NEW\s+)?(?:SHARED\s+)?(?:GLOBAL\s+)?VARIABLE\s+([\w-]+)(?:\s+AS\s+([\w.-]+))?/i.exec(line);
|
|
18
|
+
if (variable) {
|
|
19
|
+
const name = variable[1];
|
|
20
|
+
if (!variable[2]) {
|
|
21
|
+
issues.push({ code: "variable-without-type", severity: "error", message: `DEFINE VARIABLE ${name} has no AS type`, line: number });
|
|
22
|
+
}
|
|
23
|
+
if (variables.has(name)) {
|
|
24
|
+
issues.push({ code: "duplicate-variable", severity: "error", message: `Variable ${name} is defined twice`, line: number });
|
|
25
|
+
}
|
|
26
|
+
variables.add(name);
|
|
27
|
+
records.push({ id: `variable:${name}`, kind: "variable", fields: { name, type: variable[2] ?? "", line: number } });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const procedure = /^(?:PROCEDURE|FUNCTION|METHOD)\s+([\w-]+)\s*:/i.exec(line);
|
|
31
|
+
if (procedure) {
|
|
32
|
+
openers += 1;
|
|
33
|
+
records.push({ id: `procedure:${procedure[1]}`, kind: "procedure", fields: { name: procedure[1], line: number } });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (/^END\s+PROCEDURE\.?\s*$/i.test(line) || /^END\s+FUNCTION\.?\s*$/i.test(line) || /^END\s+METHOD\.?\s*$/i.test(line)) {
|
|
37
|
+
closers += 1;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (/^END\.?\s*$/i.test(line)) {
|
|
41
|
+
closers += 1;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const block = /^(FOR\s+EACH|FOR\s+FIRST|REPEAT|DO|CASE)\b/i.exec(line);
|
|
45
|
+
if (block) {
|
|
46
|
+
openers += 1;
|
|
47
|
+
records.push({ id: `block:${number}`, kind: "block", fields: { kind: block[1].toUpperCase(), line: number } });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const table = /^(?:FOR\s+EACH|FOR\s+FIRST)\s+([\w.]+)/i.exec(line);
|
|
51
|
+
if (table) {
|
|
52
|
+
tables.add(table[1]);
|
|
53
|
+
}
|
|
54
|
+
if (!line.endsWith(".") && !line.endsWith(":") && !line.endsWith("{")) {
|
|
55
|
+
issues.push({ code: "missing-period", severity: "warning", message: `Statement does not end with a period: ${line.slice(0, 40)}`, line: number });
|
|
56
|
+
}
|
|
57
|
+
records.push({ id: `stmt:${number}`, kind: "statement", fields: { text: line.slice(0, 120), line: number } });
|
|
58
|
+
});
|
|
59
|
+
if (openers !== closers) {
|
|
60
|
+
issues.push({ code: "unbalanced-blocks", severity: "error", message: `${openers} block opener(s) but ${closers} END statement(s)` });
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
format: "OpenEdge ABL",
|
|
64
|
+
records,
|
|
65
|
+
meta: {
|
|
66
|
+
variables: variables.size,
|
|
67
|
+
procedures: records.filter((record) => record.kind === "procedure").length,
|
|
68
|
+
blocks: records.filter((record) => record.kind === "block").length,
|
|
69
|
+
statements: records.filter((record) => record.kind === "statement").length,
|
|
70
|
+
},
|
|
71
|
+
issues,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const reference = [
|
|
75
|
+
"OpenEdge ABL source",
|
|
76
|
+
"",
|
|
77
|
+
"DEFINE VARIABLE cName AS CHARACTER NO-UNDO.",
|
|
78
|
+
"",
|
|
79
|
+
"PROCEDURE LoadData:",
|
|
80
|
+
" FOR EACH Customer NO-LOCK:",
|
|
81
|
+
" DISPLAY Customer.Name.",
|
|
82
|
+
" END.",
|
|
83
|
+
"END PROCEDURE.",
|
|
84
|
+
"",
|
|
85
|
+
"Statements end with a period.",
|
|
86
|
+
"PROCEDURE, FUNCTION, FOR EACH and DO open a block closed by END.",
|
|
87
|
+
"/* */ and // are comments.",
|
|
88
|
+
].join("\n");
|
|
89
|
+
export const format = {
|
|
90
|
+
slug: "openedge-abl",
|
|
91
|
+
displayName: "OpenEdge ABL",
|
|
92
|
+
extensions: [".p", ".cls", ".i"],
|
|
93
|
+
description: "Parse OpenEdge ABL source, validate variables, blocks and periods.",
|
|
94
|
+
reference,
|
|
95
|
+
parse,
|
|
96
|
+
sample: () => [
|
|
97
|
+
"DEFINE VARIABLE cName AS CHARACTER NO-UNDO.",
|
|
98
|
+
"",
|
|
99
|
+
"PROCEDURE LoadData:",
|
|
100
|
+
" FOR EACH Customer NO-LOCK:",
|
|
101
|
+
" DISPLAY Customer.Name.",
|
|
102
|
+
" END.",
|
|
103
|
+
"END PROCEDURE.",
|
|
104
|
+
].join("\n") + "\n",
|
|
105
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -5,9 +5,9 @@ async function main() {
|
|
|
5
5
|
const server = createServer();
|
|
6
6
|
const transport = new StdioServerTransport();
|
|
7
7
|
await server.connect(transport);
|
|
8
|
-
console.error(`${process.env.npm_package_name ?? "language-mcp"} running on stdio`);
|
|
9
8
|
}
|
|
10
9
|
main().catch((error) => {
|
|
11
|
-
|
|
10
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
11
|
+
process.stderr.write(`${message}\n`);
|
|
12
12
|
process.exit(1);
|
|
13
13
|
});
|
package/dist/run.d.ts
CHANGED
|
@@ -1,28 +1,17 @@
|
|
|
1
1
|
export declare class ToolError extends Error {
|
|
2
|
+
constructor(message: string);
|
|
2
3
|
}
|
|
3
|
-
export
|
|
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>;
|
|
4
|
+
export declare function errorMessage(error: unknown): string;
|
|
15
5
|
export declare function text(value: string): {
|
|
16
6
|
content: {
|
|
17
7
|
type: "text";
|
|
18
8
|
text: string;
|
|
19
9
|
}[];
|
|
20
10
|
};
|
|
21
|
-
export declare function fail(
|
|
11
|
+
export declare function fail(message: string): {
|
|
12
|
+
isError: boolean;
|
|
22
13
|
content: {
|
|
23
14
|
type: "text";
|
|
24
15
|
text: string;
|
|
25
16
|
}[];
|
|
26
|
-
isError: boolean;
|
|
27
17
|
};
|
|
28
|
-
export declare function errorMessage(error: unknown): string;
|
package/dist/run.js
CHANGED
|
@@ -1,56 +1,15 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
export class ToolError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "ToolError";
|
|
5
|
+
}
|
|
3
6
|
}
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
});
|
|
7
|
+
export function errorMessage(error) {
|
|
8
|
+
return error instanceof Error ? error.message : String(error);
|
|
47
9
|
}
|
|
48
10
|
export function text(value) {
|
|
49
11
|
return { content: [{ type: "text", text: value }] };
|
|
50
12
|
}
|
|
51
|
-
export function fail(
|
|
52
|
-
return { content: [{ type: "text", text:
|
|
53
|
-
}
|
|
54
|
-
export function errorMessage(error) {
|
|
55
|
-
return error instanceof Error ? error.message : String(error);
|
|
13
|
+
export function fail(message) {
|
|
14
|
+
return { isError: true, content: [{ type: "text", text: message }] };
|
|
56
15
|
}
|
package/dist/server.d.ts
CHANGED
package/dist/server.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { format } from "./format.js";
|
|
3
|
+
import { registerFormatTools } from "./core.js";
|
|
4
|
+
export const VERSION = "1.0.0";
|
|
4
5
|
export function createServer() {
|
|
5
|
-
const server = new McpServer({ name: `${
|
|
6
|
-
|
|
6
|
+
const server = new McpServer({ name: `${format.slug}-mcp`, version: VERSION });
|
|
7
|
+
registerFormatTools(server, format);
|
|
7
8
|
return server;
|
|
8
9
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,47 +1,31 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
export type Severity = "error" | "warning";
|
|
2
|
+
export interface Issue {
|
|
3
|
+
code: string;
|
|
4
|
+
severity: Severity;
|
|
5
|
+
message: string;
|
|
6
|
+
line?: number;
|
|
5
7
|
}
|
|
6
|
-
export interface
|
|
8
|
+
export interface ParsedRecord {
|
|
9
|
+
id: string;
|
|
7
10
|
kind: string;
|
|
8
|
-
|
|
9
|
-
flags?: string;
|
|
11
|
+
fields: Record<string, string | number | boolean | null>;
|
|
10
12
|
}
|
|
11
|
-
export interface
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export interface Snippet {
|
|
17
|
-
description: string;
|
|
18
|
-
body: string;
|
|
13
|
+
export interface Parsed {
|
|
14
|
+
format: string;
|
|
15
|
+
records: ParsedRecord[];
|
|
16
|
+
meta: Record<string, string | number | boolean | null>;
|
|
17
|
+
issues: Issue[];
|
|
19
18
|
}
|
|
20
|
-
export interface
|
|
21
|
-
path: string;
|
|
22
|
-
content: string;
|
|
23
|
-
}
|
|
24
|
-
export interface LanguageConfig {
|
|
19
|
+
export interface FormatModule {
|
|
25
20
|
slug: string;
|
|
26
21
|
displayName: string;
|
|
27
|
-
version: string;
|
|
28
22
|
extensions: string[];
|
|
29
|
-
|
|
30
|
-
comment: string;
|
|
31
|
-
blockComment?: {
|
|
32
|
-
start: string;
|
|
33
|
-
end: string;
|
|
34
|
-
};
|
|
35
|
-
stringQuotes?: string[];
|
|
36
|
-
allowTabs?: boolean;
|
|
37
|
-
buildDir?: string;
|
|
38
|
-
symbols: SymbolRule[];
|
|
39
|
-
imports: ImportRule[];
|
|
40
|
-
runner?: CommandSpec[];
|
|
41
|
-
syntax?: CommandSpec;
|
|
42
|
-
build?: CommandSpec[];
|
|
43
|
-
test?: CommandSpec[];
|
|
44
|
-
scaffold: ScaffoldFile[];
|
|
45
|
-
snippets: Record<string, Snippet>;
|
|
23
|
+
description: string;
|
|
46
24
|
reference: string;
|
|
25
|
+
parse(text: string): Parsed;
|
|
26
|
+
sample(): string;
|
|
27
|
+
regressions?: Array<{
|
|
28
|
+
doc: string;
|
|
29
|
+
expectError: string;
|
|
30
|
+
}>;
|
|
47
31
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrfentmen/openedge-abl-mcp",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "OpenEdge
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "OpenEdge MCP: parse ABL source, validate variables, blocks and statements.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"mcpName": "io.github.mrfentmen/openedge-abl-mcp",
|
|
7
7
|
"repository": {
|
|
@@ -25,9 +25,11 @@
|
|
|
25
25
|
"keywords": [
|
|
26
26
|
"mcp",
|
|
27
27
|
"model-context-protocol",
|
|
28
|
-
"openedge
|
|
29
|
-
"
|
|
30
|
-
"
|
|
28
|
+
"openedge",
|
|
29
|
+
"progress",
|
|
30
|
+
"abl",
|
|
31
|
+
"erp",
|
|
32
|
+
"4gl"
|
|
31
33
|
],
|
|
32
34
|
"license": "MIT",
|
|
33
35
|
"dependencies": {
|
package/server.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.mrfentmen/openedge-abl-mcp",
|
|
4
|
-
"description": "OpenEdge
|
|
4
|
+
"description": "OpenEdge MCP: parse ABL source, validate variables, blocks and statements.",
|
|
5
5
|
"repository": {
|
|
6
|
-
"url": "https://github.com/mrfentmen/awesome-servers
|
|
6
|
+
"url": "https://github.com/mrfentmen/awesome-servers",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.0.
|
|
9
|
+
"version": "1.0.1",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "@mrfentmen/openedge-abl-mcp",
|
|
14
|
-
"version": "1.0.
|
|
14
|
+
"version": "1.0.1",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
}
|
package/dist/config.d.ts
DELETED
package/dist/config.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
export const config = {
|
|
2
|
-
"slug": "openedge-abl",
|
|
3
|
-
"displayName": "OpenEdge ABL",
|
|
4
|
-
"version": "1.0.0",
|
|
5
|
-
"extensions": [
|
|
6
|
-
".p",
|
|
7
|
-
".cls",
|
|
8
|
-
".w"
|
|
9
|
-
],
|
|
10
|
-
"markers": [],
|
|
11
|
-
"comment": "/*",
|
|
12
|
-
"stringQuotes": [
|
|
13
|
-
"\""
|
|
14
|
-
],
|
|
15
|
-
"symbols": [
|
|
16
|
-
{
|
|
17
|
-
"kind": "function",
|
|
18
|
-
"pattern": "\\b(?:function|def|fn|fun|func|proc|procedure|sub|method|class|struct|enum|module|namespace|type|interface|trait|record|object)\\s+([A-Za-z_]\\w*)"
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
"kind": "function",
|
|
22
|
-
"pattern": "^\\s*(?:[A-Za-z_][\\w<>\\[\\]]*\\s+)+([A-Za-z_]\\w*)\\s*\\("
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
"kind": "value",
|
|
26
|
-
"pattern": "^([A-Za-z_]\\w*)\\s*(?::=|=|:)"
|
|
27
|
-
}
|
|
28
|
-
],
|
|
29
|
-
"imports": [],
|
|
30
|
-
"scaffold": [
|
|
31
|
-
{
|
|
32
|
-
"path": "src/demo.p",
|
|
33
|
-
"content": "DEFINE VARIABLE x AS INTEGER NO-UNDO.\n"
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
"path": "tests/run.p",
|
|
37
|
-
"content": "DEFINE VARIABLE x AS INTEGER NO-UNDO.\n"
|
|
38
|
-
}
|
|
39
|
-
],
|
|
40
|
-
"snippets": {
|
|
41
|
-
"function": {
|
|
42
|
-
"description": "function",
|
|
43
|
-
"body": "DEFINE VARIABLE x AS INTEGER NO-UNDO.\n"
|
|
44
|
-
}
|
|
45
|
-
},
|
|
46
|
-
"reference": "OpenEdge ABL syntax reference\n- Example: DEFINE VARIABLE x AS INTEGER NO-UNDO.\n- Comment: /*\n- No CLI runner configured"
|
|
47
|
-
};
|