@mrfentmen/ini-mcp 0.0.0-stage → 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 +33 -2
- package/dist/api.d.ts +19 -0
- package/dist/api.js +205 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +13 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +53 -0
- package/package.json +41 -4
package/README.md
CHANGED
|
@@ -1,3 +1,34 @@
|
|
|
1
|
-
#
|
|
1
|
+
# INI Files
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Parse, validate, convert and generate INI configuration offline - sections, comments and quoted values, keyless.
|
|
4
|
+
|
|
5
|
+
Keyless — no signup, no API key, no environment variables.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx -y @mrfentmen/ini-mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or add it to your MCP client:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"mcpServers": {
|
|
18
|
+
"ini-mcp": { "command": "npx", "args": ["-y", "@mrfentmen/ini-mcp"] }
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Tools
|
|
24
|
+
|
|
25
|
+
| Tool | What it does |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `parse_ini` | Parse INI text into sections and keys, with inline comments handled and problems reported. |
|
|
28
|
+
| `ini_to_json` | Convert INI text into a JSON object keyed by section. |
|
|
29
|
+
| `create_ini` | Build INI text from a JSON object: scalars go at the top, nested objects become sections. |
|
|
30
|
+
| `validate_ini` | Check INI text for structural problems: bad lines, unterminated quotes, duplicate sections and keys. |
|
|
31
|
+
|
|
32
|
+
## Data source
|
|
33
|
+
|
|
34
|
+
Runs fully offline — all work happens inside the server with no API calls and no keys.
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* INI Files — keyless public API client.
|
|
3
|
+
*/
|
|
4
|
+
export declare class SourceError extends Error {
|
|
5
|
+
}
|
|
6
|
+
export declare function section(title: string, rows: string[]): string;
|
|
7
|
+
export declare function parseIniFile(a: {
|
|
8
|
+
text: string;
|
|
9
|
+
}): Promise<string>;
|
|
10
|
+
export declare function iniToJson(a: {
|
|
11
|
+
text: string;
|
|
12
|
+
mergeDuplicateSections?: boolean;
|
|
13
|
+
}): Promise<string>;
|
|
14
|
+
export declare function createIni(a: {
|
|
15
|
+
json: string;
|
|
16
|
+
}): Promise<string>;
|
|
17
|
+
export declare function validateIni(a: {
|
|
18
|
+
text: string;
|
|
19
|
+
}): Promise<string>;
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* INI Files — keyless public API client.
|
|
3
|
+
*/
|
|
4
|
+
export class SourceError extends Error {
|
|
5
|
+
}
|
|
6
|
+
export function section(title, rows) {
|
|
7
|
+
return [title, ...rows.filter(Boolean)].join("\n");
|
|
8
|
+
}
|
|
9
|
+
function stripInlineComment(value) {
|
|
10
|
+
let inQuote = null;
|
|
11
|
+
for (let i = 0; i < value.length; i++) {
|
|
12
|
+
const ch = value[i];
|
|
13
|
+
if (inQuote) {
|
|
14
|
+
if (ch === inQuote)
|
|
15
|
+
inQuote = null;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (ch === '"' || ch === "'") {
|
|
19
|
+
inQuote = ch;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if ((ch === ";" || ch === "#") && (i === 0 || /\s/.test(value[i - 1]))) {
|
|
23
|
+
return value.slice(0, i).trimEnd();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return value.trimEnd();
|
|
27
|
+
}
|
|
28
|
+
function unquote(value) {
|
|
29
|
+
const trimmed = value.trim();
|
|
30
|
+
if (trimmed.length >= 2 && ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'")))) {
|
|
31
|
+
return trimmed.slice(1, -1);
|
|
32
|
+
}
|
|
33
|
+
return trimmed;
|
|
34
|
+
}
|
|
35
|
+
function parseIni(text) {
|
|
36
|
+
const entries = [];
|
|
37
|
+
const errors = [];
|
|
38
|
+
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
39
|
+
lines.forEach((raw, index) => {
|
|
40
|
+
const lineNo = index + 1;
|
|
41
|
+
const line = raw.trim();
|
|
42
|
+
if (!line)
|
|
43
|
+
return;
|
|
44
|
+
if (line.startsWith(";") || line.startsWith("#")) {
|
|
45
|
+
entries.push({ kind: "comment", text: line });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (line.startsWith("[")) {
|
|
49
|
+
if (!line.endsWith("]")) {
|
|
50
|
+
errors.push(`line ${lineNo}: section header is missing its closing bracket`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const name = line.slice(1, -1).trim();
|
|
54
|
+
if (!name)
|
|
55
|
+
errors.push(`line ${lineNo}: section name is empty`);
|
|
56
|
+
entries.push({ kind: "section", name });
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const eq = line.indexOf("=");
|
|
60
|
+
if (eq < 0) {
|
|
61
|
+
const colon = line.indexOf(":");
|
|
62
|
+
if (colon > 0) {
|
|
63
|
+
const key = line.slice(0, colon).trim();
|
|
64
|
+
const value = stripInlineComment(line.slice(colon + 1));
|
|
65
|
+
entries.push({ kind: "pair", key, value: unquote(value) });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
errors.push(`line ${lineNo}: not a key=value pair: "${line.slice(0, 40)}"`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const key = line.slice(0, eq).trim();
|
|
72
|
+
if (!key) {
|
|
73
|
+
errors.push(`line ${lineNo}: the key before "=" is empty`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const value = stripInlineComment(line.slice(eq + 1));
|
|
77
|
+
if (unquote(value).length === 0 && value.trim() !== "" && !/^["']{2}$/.test(value.trim())) {
|
|
78
|
+
errors.push(`line ${lineNo}: the value has an unterminated quote`);
|
|
79
|
+
}
|
|
80
|
+
entries.push({ kind: "pair", key, value: unquote(value) });
|
|
81
|
+
});
|
|
82
|
+
return { entries, errors };
|
|
83
|
+
}
|
|
84
|
+
function group(entries) {
|
|
85
|
+
const sections = [];
|
|
86
|
+
let current = { name: "", pairs: [] };
|
|
87
|
+
sections.push(current);
|
|
88
|
+
for (const entry of entries) {
|
|
89
|
+
if (entry.kind === "section") {
|
|
90
|
+
current = { name: entry.name, pairs: [] };
|
|
91
|
+
sections.push(current);
|
|
92
|
+
}
|
|
93
|
+
else if (entry.kind === "pair") {
|
|
94
|
+
current.pairs.push({ key: entry.key, value: entry.value });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return sections.filter((s) => s.name || s.pairs.length);
|
|
98
|
+
}
|
|
99
|
+
export async function parseIniFile(a) {
|
|
100
|
+
const { entries, errors } = parseIni(a.text);
|
|
101
|
+
const sections = group(entries);
|
|
102
|
+
if (!entries.length)
|
|
103
|
+
throw new SourceError("no ini content found");
|
|
104
|
+
const lines = [];
|
|
105
|
+
for (const section of sections) {
|
|
106
|
+
lines.push(section.name ? `[${section.name}]` : "(top level, no section)");
|
|
107
|
+
for (const pair of section.pairs)
|
|
108
|
+
lines.push(` ${pair.key} = ${pair.value}`);
|
|
109
|
+
lines.push("");
|
|
110
|
+
}
|
|
111
|
+
return [
|
|
112
|
+
`${sections.length} section(s), ${entries.filter((e) => e.kind === "pair").length} key(s):`,
|
|
113
|
+
"",
|
|
114
|
+
...lines,
|
|
115
|
+
...(errors.length ? ["Problems:", ...errors.map((e) => `- ${e}`)] : []),
|
|
116
|
+
].join("\n").trimEnd();
|
|
117
|
+
}
|
|
118
|
+
export async function iniToJson(a) {
|
|
119
|
+
const { entries, errors } = parseIni(a.text);
|
|
120
|
+
if (errors.length)
|
|
121
|
+
throw new SourceError(`the ini text has problems: ${errors.join("; ")}`);
|
|
122
|
+
const sections = group(entries);
|
|
123
|
+
const out = {};
|
|
124
|
+
for (const section of sections) {
|
|
125
|
+
const name = section.name || "default";
|
|
126
|
+
for (const pair of section.pairs) {
|
|
127
|
+
out[name] = out[name] ?? {};
|
|
128
|
+
if (pair.key in out[name] && !a.mergeDuplicateSections) {
|
|
129
|
+
throw new SourceError(`key "${pair.key}" appears twice in section "${name}"`);
|
|
130
|
+
}
|
|
131
|
+
out[name][pair.key] = pair.value;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return [
|
|
135
|
+
"Parsed ini as JSON:",
|
|
136
|
+
"",
|
|
137
|
+
JSON.stringify(out, null, 2),
|
|
138
|
+
].join("\n");
|
|
139
|
+
}
|
|
140
|
+
export async function createIni(a) {
|
|
141
|
+
let data;
|
|
142
|
+
try {
|
|
143
|
+
data = JSON.parse(a.json);
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
throw new SourceError(`json is not valid: ${e.message}`);
|
|
147
|
+
}
|
|
148
|
+
if (data === null || typeof data !== "object" || Array.isArray(data)) {
|
|
149
|
+
throw new SourceError("json must be an object of key/value pairs and nested objects");
|
|
150
|
+
}
|
|
151
|
+
const scalars = (value) => value === null || ["string", "number", "boolean"].includes(typeof value);
|
|
152
|
+
const lines = [];
|
|
153
|
+
const render = (value) => (value === null ? "" : String(value));
|
|
154
|
+
for (const [key, value] of Object.entries(data)) {
|
|
155
|
+
if (scalars(value))
|
|
156
|
+
lines.push(`${key} = ${render(value)}`);
|
|
157
|
+
}
|
|
158
|
+
for (const [section, value] of Object.entries(data)) {
|
|
159
|
+
if (scalars(value))
|
|
160
|
+
continue;
|
|
161
|
+
if (Array.isArray(value)) {
|
|
162
|
+
lines.push("", `[${section}]`, `values = ${value.map(render).join(",")}`);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (value !== null && typeof value === "object") {
|
|
166
|
+
lines.push("", `[${section}]`);
|
|
167
|
+
for (const [key, inner] of Object.entries(value)) {
|
|
168
|
+
if (inner !== null && typeof inner === "object")
|
|
169
|
+
throw new SourceError(`section "${section}" key "${key}" nests too deeply for an ini file`);
|
|
170
|
+
lines.push(`${key} = ${render(inner)}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (!lines.length)
|
|
175
|
+
throw new SourceError("json has no values to write");
|
|
176
|
+
return [`Generated ini (${lines.length} line(s)):`, "", lines.join("\n")].join("\n");
|
|
177
|
+
}
|
|
178
|
+
export async function validateIni(a) {
|
|
179
|
+
const { entries, errors } = parseIni(a.text);
|
|
180
|
+
const problems = [...errors];
|
|
181
|
+
const seenSections = new Set();
|
|
182
|
+
let current = "";
|
|
183
|
+
const seenKeys = new Set();
|
|
184
|
+
for (const entry of entries) {
|
|
185
|
+
if (entry.kind === "section") {
|
|
186
|
+
if (seenSections.has(entry.name))
|
|
187
|
+
problems.push(`section [${entry.name}] is defined more than once`);
|
|
188
|
+
seenSections.add(entry.name);
|
|
189
|
+
current = entry.name;
|
|
190
|
+
seenKeys.clear();
|
|
191
|
+
}
|
|
192
|
+
else if (entry.kind === "pair") {
|
|
193
|
+
const id = `${current}\u0000${entry.key}`;
|
|
194
|
+
if (seenKeys.has(entry.key))
|
|
195
|
+
problems.push(`key "${entry.key}" appears twice${current ? ` in [${current}]` : " at the top level"}`);
|
|
196
|
+
seenKeys.add(entry.key);
|
|
197
|
+
void id;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return [
|
|
201
|
+
problems.length ? `Invalid ini — ${problems.length} problem(s):` : "Valid ini.",
|
|
202
|
+
"",
|
|
203
|
+
...problems.map((p) => `- ${p}`),
|
|
204
|
+
].join("\n").trimEnd();
|
|
205
|
+
}
|
package/dist/index.d.ts
ADDED
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("ini-mcp running on stdio");
|
|
9
|
+
}
|
|
10
|
+
main().catch((err) => {
|
|
11
|
+
console.error("Fatal error:", err);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
});
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createIni, iniToJson, parseIniFile, validateIni, SourceError } from "./api.js";
|
|
4
|
+
const text = (t) => ({ content: [{ type: "text", text: t }] });
|
|
5
|
+
function errorMessage(e) {
|
|
6
|
+
if (e instanceof SourceError || e instanceof Error)
|
|
7
|
+
return `Error: ${e.message}`;
|
|
8
|
+
return `Error: ${String(e)}`;
|
|
9
|
+
}
|
|
10
|
+
export function createServer() {
|
|
11
|
+
const server = new McpServer({ name: "ini-mcp", version: "1.0.0" });
|
|
12
|
+
server.tool("parse_ini", "Parse INI text into sections and keys, with inline comments handled and problems reported.", {
|
|
13
|
+
text: z.string().describe('INI text')
|
|
14
|
+
}, async (a) => {
|
|
15
|
+
try {
|
|
16
|
+
return text(await parseIniFile(a));
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
return { content: [{ type: "text", text: errorMessage(e) }], isError: true };
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
server.tool("ini_to_json", "Convert INI text into a JSON object keyed by section.", {
|
|
23
|
+
text: z.string().describe('INI text'), mergeDuplicateSections: z.boolean().optional().describe('Allow a section to appear more than once')
|
|
24
|
+
}, async (a) => {
|
|
25
|
+
try {
|
|
26
|
+
return text(await iniToJson(a));
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
return { content: [{ type: "text", text: errorMessage(e) }], isError: true };
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
server.tool("create_ini", "Build INI text from a JSON object: scalars go at the top, nested objects become sections.", {
|
|
33
|
+
json: z.string().describe('JSON object to turn into INI')
|
|
34
|
+
}, async (a) => {
|
|
35
|
+
try {
|
|
36
|
+
return text(await createIni(a));
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
return { content: [{ type: "text", text: errorMessage(e) }], isError: true };
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
server.tool("validate_ini", "Check INI text for structural problems: bad lines, unterminated quotes, duplicate sections and keys.", {
|
|
43
|
+
text: z.string().describe('INI text')
|
|
44
|
+
}, async (a) => {
|
|
45
|
+
try {
|
|
46
|
+
return text(await validateIni(a));
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
return { content: [{ type: "text", text: errorMessage(e) }], isError: true };
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
return server;
|
|
53
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrfentmen/ini-mcp",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Parse, validate, convert and generate INI configuration offline - sections, comments and quoted values, keyless.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"mcpName": "io.github.mrfentmen/ini-mcp",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/mrfentmen/awesome-servers.git"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"ini-mcp": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.json",
|
|
20
|
+
"start": "node dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"mcp",
|
|
24
|
+
"ini",
|
|
25
|
+
"config",
|
|
26
|
+
"parser",
|
|
27
|
+
"settings",
|
|
28
|
+
"offline",
|
|
29
|
+
"keyless"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
34
|
+
"zod": "^3.23.8"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^22.0.0",
|
|
38
|
+
"typescript": "^5.6.0"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=20"
|
|
42
|
+
}
|
|
43
|
+
}
|