Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions mcp_modules/csvjson/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# mcp-module-csvjson

Lossless **CSV ↔ JSON** conversion for agents: schema inference, strict coercion,
nested-object flattening, union-of-columns protection and a **reconciliation
receipt** with every conversion.

Zero runtime dependencies. Node >= 20.

## Why

Naive CSV→JSON converters silently corrupt data: `"00123"` becomes `123`,
`"true"` becomes a string in one table and a boolean in another, mixed columns
half-parse, and nested records (arrays, objects) collapse into ambiguous
strings. This module makes every conversion **explicit**:

- columns get an inferred type (`null < bool < int < float < str < json`),
- a value that cannot coerce raises `SchemaError` instead of being mangled,
- dotted keys (`tags.0`, `meta.role`) flatten and expand losslessly,
- heterogeneous records survive via union-of-columns rules: blank cells in
nested groups are ignored, container keys never receive scalars,
- every conversion returns a receipt: rows/fields in→out, missing/added
fields, changed cells, null counts, and an `ok` flag.

## Endpoints

| Method | Path | Purpose |
|--------|------|---------|
| GET | `/csvjson` | module info + guarantees |
| POST | `/csvjson/to-json` | `{ csv, options? }` → `{ data, schema, receipt }` |
| POST | `/csvjson/to-csv` | `{ rows, options? }` → `{ csv, columns, receipt }` |
| POST | `/csvjson/schema` | `{ csv }` → inferred schema |
| POST | `/csvjson/validate` | `{ csv }` → `{ valid, rows, schema? , error? }` |

Options: `{ delimiter?: string (default ","), strict?: boolean (default true), header?: boolean (default true) }`.

## Example

```js
const { csvToJson } = await import('./src/service.js');

const csv = `name,age,active,score\nalice,30,true,9.5\nbob,42,false,8.25`;
const { data, schema, receipt } = csvToJson(csv);
// schema: { name: 'str', age: 'int', active: 'bool', score: 'float' }
// data: [{ name: 'alice', age: 30, active: true, score: 9.5 }, ...]
// receipt.ok: true
```

Nested round-trip:

```js
const rows = [
{ id: 1, meta: { role: 'admin', tags: ['x', 'y'] } },
{ id: 2, meta: { role: 'user', tags: ['z'] } },
];
const { csv } = jsonToCsv(rows);
const back = csvToJson(csv);
// back.data deep-equals rows; back.receipt.ok === true
```

## Test

```bash
npm test # mocha test/**/*.test.js
```
65 changes: 65 additions & 0 deletions mcp_modules/csvjson/docs/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# csvjson API

## POST /csvjson/to-json

Body:
```json
{
"csv": "name,age\nalice,30\nbob,42",
"options": { "delimiter": ",", "strict": true, "header": true }
}
```

Response `200`:
```json
{
"rows": 2,
"schema": { "name": "str", "age": "int" },
"data": [ { "name": "alice", "age": 30 }, { "name": "bob", "age": 42 } ],
"receipt": {
"rowsIn": 2, "rowsOut": 2, "fieldsIn": 2, "fieldsOut": 2,
"missingFields": [], "addedFields": [], "changedCells": 0,
"nullCounts": {}, "ok": true
}
}
```

Response `422` (strict coercion failure):
```json
{ "error": "column \"age\" expects int, got \"abc\": not an integer", "name": "SchemaError" }
```

## POST /csvjson/to-csv

Body:
```json
{
"rows": [ { "id": 1, "tags": ["a", "b"] } ],
"options": { "delimiter": "," }
}
```

Response:
```json
{
"csv": "id,tags.0,tags.1\n1,a,b",
"columns": ["id", "tags.0", "tags.1"],
"rows": 1,
"receipt": { "rowsIn": 1, "rowsOut": 1, "fieldsIn": 2, "fieldsOut": 3, "missingFields": [], "addedFields": [], "changedCells": 0, "nullCounts": {}, "ok": true }
}
```

## POST /csvjson/schema

Body `{ "csv": "a\n1\n2" }` → `{ "schema": { "a": "int" }, "rows": 2 }`.

## POST /csvjson/validate

Body `{ "csv": "a\nabc" }` → `{ "valid": false, "error": "column \"a\" expects int, got \"abc\"...", "name": "SchemaError" }`.

## Guarantees

1. **Strict coercion** — mixed or unparsable columns fail loudly (`SchemaError`), never silently coerce.
2. **Lossless nested round-trip** — `jsonToCsv` then `csvToJson` returns deep-equal records for well-formed data.
3. **Union of columns** — blanks in nested groups are ignored; container keys (`tags`) never receive scalars; scalar blanks stay real nulls.
4. **Receipts** — rows/fields in→out, missing/added fields, changed cells, null counts, `ok`.
17 changes: 17 additions & 0 deletions mcp_modules/csvjson/examples/basic-usage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// basic-usage.js — csvjson module examples
import { csvToJson, jsonToCsv, validate } from '../src/service.js';

const csv = `product,qty,price,active
widget,3,9.99,true
gadget,1,49.5,false`;

const { data, schema, receipt } = csvToJson(csv);
console.log('schema:', schema);
console.log('data:', JSON.stringify(data));
console.log('receipt ok:', receipt.ok);

const nested = [{ id: 1, meta: { tags: ['a', 'b'] } }, { id: 2, meta: { tags: [] } }];
const { csv: out } = jsonToCsv(nested);
console.log('csv:', '\n' + out);

console.log('validate bad:', validate('x\nabc\n'));
66 changes: 66 additions & 0 deletions mcp_modules/csvjson/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* csvjson Module
*
* Lossless CSV <-> JSON conversion: schema inference, strict coercion,
* nested-object flattening, union-of-columns protection and reconciliation
* receipts. Zero runtime dependencies.
*/

import { logger } from '../../src/utils/logger.js';
import { toJson, toCsv, schema, validateCsv, info } from './src/controller.js';
import { version } from './package.json' with { type: 'json' };

/**
* Register this module with the Hono app
* @param {import('hono').Hono} app - The Hono app instance
*/
export async function register(app) {
logger.info('Registering csvjson module');

app.get('/csvjson', info);

app.post('/csvjson/to-json', toJson);
app.post('/csvjson/to-csv', toCsv);
app.post('/csvjson/schema', schema);
app.post('/csvjson/validate', validateCsv);

app.get('/tools/csvjson/info', (c) => {
return c.json({
name: 'csvjson',
description:
'Lossless CSV <-> JSON conversion with schema inference, strict coercion and ' +
'reconciliation receipts. Use to convert tabular data without silent corruption.',
version,
});
});

app.post('/tools/csvjson/to_json', (c) => toJson(c));
app.post('/tools/csvjson/to_csv', (c) => toCsv(c));
app.post('/tools/csvjson/validate', (c) => validateCsv(c));

app.get('/tools/csvjson/to_json/info', (c) => {
return c.json({
name: 'csvjson_to_json',
description: 'Convert CSV text to JSON records. Returns inferred schema and a reconciliation receipt.',
parameters: {
csv: { type: 'string', description: 'The CSV text to convert', required: true },
options: {
type: 'object',
description: '{ delimiter?: string, strict?: boolean, header?: boolean }',
required: false,
},
},
});
});

app.get('/tools/csvjson/to_csv/info', (c) => {
return c.json({
name: 'csvjson_to_csv',
description: 'Convert an array of JSON records to lossless CSV text (union of all columns).',
parameters: {
rows: { type: 'array', description: 'Array of JSON objects', required: true },
options: { type: 'object', description: '{ delimiter?: string }', required: false },
},
});
});
}
35 changes: 35 additions & 0 deletions mcp_modules/csvjson/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "mcp-module-csvjson",
"version": "1.0.0",
"description": "Lossless CSV <-> JSON conversion with schema inference, strict coercion, nested-object flattening and reconciliation receipts",
"main": "index.js",
"type": "module",
"scripts": {
"test": "mocha test/**/*.test.js",
"test:watch": "mocha test/**/*.test.js --watch",
"lint": "eslint src/ test/ --fix",
"format": "prettier --write src/ test/ examples/"
},
"keywords": [
"mcp",
"module",
"csv",
"json",
"conversion",
"schema",
"data"
],
"author": "profullstack community",
"license": "ISC",
"engines": {
"node": ">=20.10.0"
},
"dependencies": {},
"devDependencies": {
"chai": "^4.3.7",
"mocha": "^10.2.0",
"sinon": "^17.0.1",
"eslint": "^8.57.0",
"prettier": "^3.0.0"
}
}
115 changes: 115 additions & 0 deletions mcp_modules/csvjson/src/controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/**
* HTTP handlers for the csvjson module.
*/

import { csvToJson, jsonToCsv, validate } from './service.js';

/**
* POST /csvjson/to-json { csv, options? } -> { data, schema, receipt }
*/
export async function toJson(c) {
try {
const body = await c.req.json();
if (typeof body.csv !== 'string') {
return c.json({ error: 'Missing required parameter: csv (string)' }, 400);
}
const options = body.options && typeof body.options === 'object' ? body.options : {};
if (options.delimiter && (typeof options.delimiter !== 'string' || options.delimiter.length !== 1)) {
return c.json({ error: 'options.delimiter must be a single character' }, 400);
}
const result = csvToJson(body.csv, options);
return c.json({
rows: result.data.length,
schema: result.schema,
data: result.data,
receipt: result.receipt,
timestamp: new Date().toISOString(),
});
} catch (err) {
return c.json({ error: err.message, name: err.name || 'Error' }, 422);
}
}

/**
* POST /csvjson/to-csv { rows, options? } -> { csv, columns, receipt }
*/
export async function toCsv(c) {
try {
const body = await c.req.json();
const rows = body.rows ?? body.data;
if (!Array.isArray(rows)) {
return c.json({ error: 'Missing required parameter: rows (array of objects)' }, 400);
}
if (rows.some((r) => r === null || typeof r !== 'object' || Array.isArray(r))) {
return c.json({ error: 'rows must contain only objects' }, 400);
}
const options = body.options && typeof body.options === 'object' ? body.options : {};
const { csv, columns } = jsonToCsv(rows, options);
const fieldsIn = new Set(rows.flatMap((r) => Object.keys(r))).size;
return c.json({
csv,
columns,
rows: rows.length,
receipt: {
rowsIn: rows.length,
rowsOut: rows.length,
fieldsIn,
fieldsOut: columns.length,
missingFields: [],
addedFields: columns.length > fieldsIn ? columns.filter((col) => !Object.keys(rows[0] || {}).includes(col)) : [],
changedCells: 0,
nullCounts: {},
ok: true,
},
timestamp: new Date().toISOString(),
});
} catch (err) {
return c.json({ error: err.message, name: err.name || 'Error' }, 422);
}
}

/**
* POST /csvjson/schema { csv, options? } -> { schema, rows }
*/
export async function schema(c) {
try {
const body = await c.req.json();
if (typeof body.csv !== 'string') return c.json({ error: 'Missing required parameter: csv' }, 400);
const result = csvToJson(body.csv, body.options || {});
return c.json({ schema: result.schema, rows: result.data.length, timestamp: new Date().toISOString() });
} catch (err) {
return c.json({ error: err.message }, 422);
}
}

/**
* POST /csvjson/validate { csv, options? } -> { valid, rows, schema?, error? }
*/
export async function validateCsv(c) {
try {
const body = await c.req.json();
if (typeof body.csv !== 'string') return c.json({ error: 'Missing required parameter: csv' }, 400);
const result = validate(body.csv, body.options || {});
return c.json({ ...result, timestamp: new Date().toISOString() });
} catch (err) {
return c.json({ error: err.message }, 500);
}
}

/**
* GET /csvjson/schema introspection for the module.
*/
export function info(c) {
return c.json({
module: 'csvjson',
status: 'active',
message: 'Lossless CSV <-> JSON conversion with schema inference and reconciliation receipts',
operations: ['/csvjson/to-json', '/csvjson/to-csv', '/csvjson/schema', '/csvjson/validate'],
guarantees: [
'strict coercion: mixed columns fail loudly with SchemaError (no silent corruption)',
'nested objects and arrays flatten to dotted keys and expand back',
'union-of-columns: blank cells in nested groups are ignored; container keys never receive scalars',
'every conversion returns a reconciliation receipt (rows/fields in-out, missing/added fields, changed cells, ok)',
],
});
}
Loading
Loading