@chaprola/mcp-server 1.13.0 → 1.14.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/dist/index.js +59 -20
- package/package.json +1 -1
- package/references/cookbook.md +97 -0
- package/references/gotchas.md +30 -9
package/dist/index.js
CHANGED
|
@@ -96,7 +96,7 @@ const server = new McpServer({
|
|
|
96
96
|
**What you can do:**
|
|
97
97
|
- **Import data:** chaprola_import (JSON or FHIR bundles), chaprola_import_download (CSV/Excel/Parquet from URL)
|
|
98
98
|
- **Query data:** chaprola_query (filter, aggregate, join, pivot — like SELECT without SQL)
|
|
99
|
-
- **Record CRUD:** chaprola_insert_record, chaprola_update_record, chaprola_delete_record
|
|
99
|
+
- **Record CRUD:** chaprola_insert_record (single or batch), chaprola_upsert_record (insert-or-update by key), chaprola_update_record, chaprola_delete_record
|
|
100
100
|
- **Batch operations:** chaprola_run_each — run a compiled program against every record in a file (like a stored procedure that executes per-row). Use this for scoring, bulk updates, conditional logic across records.
|
|
101
101
|
- **Compile programs:** chaprola_compile (source code → bytecode). Programs are stored procedures — compile once, run on demand.
|
|
102
102
|
- **Run programs:** chaprola_run (single execution), chaprola_run_each (per-record batch), chaprola_report (published reports)
|
|
@@ -911,6 +911,25 @@ server.tool("chaprola_insert_record", "Insert one or more records into a data fi
|
|
|
911
911
|
const res = await authedFetch("/insert-record", body);
|
|
912
912
|
return textResult(res);
|
|
913
913
|
}));
|
|
914
|
+
server.tool("chaprola_upsert_record", "Insert or update a record by key field. If a record with the matching key value exists, update it. If not, insert it. Batch supported via records array.", {
|
|
915
|
+
project: z.string().describe("Project name"),
|
|
916
|
+
file: z.string().describe("Data file name (without extension)"),
|
|
917
|
+
key: z.string().describe("Field name to match on (must exist in format file)"),
|
|
918
|
+
record: z.string().optional().describe("JSON object of a single record. Must contain the key field. Use this OR records, not both."),
|
|
919
|
+
records: z.string().optional().describe("JSON array of records for batch upsert. Each must contain the key field. Use this OR record, not both."),
|
|
920
|
+
userid: z.string().optional().describe("Project owner's username. Required when accessing a shared project where you are a writer. Defaults to the authenticated user."),
|
|
921
|
+
}, async ({ project, file, key, record: recordStr, records: recordsStr, userid }) => withBaaCheck(async () => {
|
|
922
|
+
const record = recordStr ? (typeof recordStr === 'string' ? JSON.parse(recordStr) : recordStr) : undefined;
|
|
923
|
+
const records = recordsStr ? (typeof recordsStr === 'string' ? JSON.parse(recordsStr) : recordsStr) : undefined;
|
|
924
|
+
const { username } = getCredentials();
|
|
925
|
+
const body = { userid: userid || username, project, file, key };
|
|
926
|
+
if (record)
|
|
927
|
+
body.record = record;
|
|
928
|
+
if (records)
|
|
929
|
+
body.records = records;
|
|
930
|
+
const res = await authedFetch("/upsert-record", body);
|
|
931
|
+
return textResult(res);
|
|
932
|
+
}));
|
|
914
933
|
server.tool("chaprola_update_record", "Update fields in a single record matched by a where clause. If no sort-key changes, updates in place; otherwise marks old record ignored and appends to merge file.", {
|
|
915
934
|
project: z.string().describe("Project name"),
|
|
916
935
|
file: z.string().describe("Data file name (without extension)"),
|
|
@@ -935,25 +954,6 @@ server.tool("chaprola_delete_record", "Delete a single record matched by a where
|
|
|
935
954
|
const res = await authedFetch("/delete-record", { userid: userid || username, project, file, where: whereClause });
|
|
936
955
|
return textResult(res);
|
|
937
956
|
}));
|
|
938
|
-
server.tool("chaprola_upsert_record", "Insert or update a record by key field. If a record with the matching key value exists, update it in place. If not, insert it. Supports batch: pass records array to upsert multiple records in one call.", {
|
|
939
|
-
project: z.string().describe("Project name"),
|
|
940
|
-
file: z.string().describe("Data file name (without extension)"),
|
|
941
|
-
key: z.string().describe("Field name to match on, e.g. \"contact_id\". Must exist in the format file."),
|
|
942
|
-
record: z.string().optional().describe("JSON object of a single record to upsert, e.g. {\"contact_id\": \"c-42\", \"name\": \"Alice\"}. Must contain the key field. Use this OR records, not both."),
|
|
943
|
-
records: z.string().optional().describe("JSON array of records for batch upsert (max 1000). Each must contain the key field. Use this OR record, not both."),
|
|
944
|
-
userid: z.string().optional().describe("Project owner's username. Required when accessing a shared project where you are a writer. Defaults to the authenticated user."),
|
|
945
|
-
}, async ({ project, file, key, record: recordStr, records: recordsStr, userid }) => withBaaCheck(async () => {
|
|
946
|
-
const record = recordStr ? (typeof recordStr === 'string' ? JSON.parse(recordStr) : recordStr) : undefined;
|
|
947
|
-
const records = recordsStr ? (typeof recordsStr === 'string' ? JSON.parse(recordsStr) : recordsStr) : undefined;
|
|
948
|
-
const { username } = getCredentials();
|
|
949
|
-
const body = { userid: userid || username, project, file, key };
|
|
950
|
-
if (record)
|
|
951
|
-
body.record = record;
|
|
952
|
-
if (records)
|
|
953
|
-
body.records = records;
|
|
954
|
-
const res = await authedFetch("/upsert-record", body);
|
|
955
|
-
return textResult(res);
|
|
956
|
-
}));
|
|
957
957
|
server.tool("chaprola_consolidate", "Merge a .MRG file into its parent .DA, producing a clean sorted data file. Deletes .MRG and .IGN after success. Aborts if .MRG was modified during the operation.", {
|
|
958
958
|
project: z.string().describe("Project name"),
|
|
959
959
|
file: z.string().describe("Data file name (without extension)"),
|
|
@@ -1024,6 +1024,45 @@ server.tool("chaprola_app_upload", "Get a presigned URL to upload a single file
|
|
|
1024
1024
|
const res = await authedFetch("/app/upload", { userid: username, project, path });
|
|
1025
1025
|
return textResult(res);
|
|
1026
1026
|
}));
|
|
1027
|
+
server.tool("chaprola_app_upload_inline", "Upload a single file's contents inline (no presigned PUT, no curl step). Use this for hot-fixes to one HTML/JS/CSS file. Max 3 MB raw payload. For larger files, use chaprola_app_upload.", {
|
|
1028
|
+
project: z.string().describe("Project name for the app"),
|
|
1029
|
+
path: z.string().describe("File path within the app (e.g. 'css/app.css', 'results.html')"),
|
|
1030
|
+
content: z.string().describe("File contents. Plain text by default; pass encoding='base64' for binary files."),
|
|
1031
|
+
encoding: z.enum(["text", "base64"]).optional().describe("Content encoding: 'text' (default) or 'base64' for binary files"),
|
|
1032
|
+
content_type: z.string().optional().describe("MIME type override. Inferred from path extension if omitted."),
|
|
1033
|
+
userid: z.string().optional().describe("Project owner's username. Required when deploying to a shared project where you are a writer. Defaults to the authenticated user."),
|
|
1034
|
+
}, async ({ project, path, content, encoding, content_type, userid }) => withBaaCheck(async () => {
|
|
1035
|
+
const { username } = getCredentials();
|
|
1036
|
+
const body = {
|
|
1037
|
+
userid: userid || username,
|
|
1038
|
+
project,
|
|
1039
|
+
path,
|
|
1040
|
+
content,
|
|
1041
|
+
};
|
|
1042
|
+
if (encoding)
|
|
1043
|
+
body.encoding = encoding;
|
|
1044
|
+
if (content_type)
|
|
1045
|
+
body.content_type = content_type;
|
|
1046
|
+
const res = await authedFetch("/app/upload/inline", body);
|
|
1047
|
+
return textResult(res);
|
|
1048
|
+
}));
|
|
1049
|
+
server.tool("chaprola_app_deploy_inline", "Deploy a base64-encoded tar.gz or zip archive in one call — no presigned PUT, no curl step. Max 3 MB raw (~4 MB base64). For larger bundles use chaprola_app_deploy.", {
|
|
1050
|
+
project: z.string().describe("Project name for the app"),
|
|
1051
|
+
archive: z.string().describe("Base64-encoded tar.gz or zip archive of the app directory"),
|
|
1052
|
+
archive_format: z.enum(["tar.gz", "zip"]).optional().describe("Archive format. Auto-detected from magic bytes if omitted."),
|
|
1053
|
+
userid: z.string().optional().describe("Project owner's username. Required when deploying to a shared project where you are a writer. Defaults to the authenticated user."),
|
|
1054
|
+
}, async ({ project, archive, archive_format, userid }) => withBaaCheck(async () => {
|
|
1055
|
+
const { username } = getCredentials();
|
|
1056
|
+
const body = {
|
|
1057
|
+
userid: userid || username,
|
|
1058
|
+
project,
|
|
1059
|
+
archive,
|
|
1060
|
+
};
|
|
1061
|
+
if (archive_format)
|
|
1062
|
+
body.archive_format = archive_format;
|
|
1063
|
+
const res = await authedFetch("/app/deploy/inline", body);
|
|
1064
|
+
return textResult(res);
|
|
1065
|
+
}));
|
|
1027
1066
|
// --- Start server ---
|
|
1028
1067
|
async function main() {
|
|
1029
1068
|
const transport = new StdioServerTransport();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chaprola/mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"description": "MCP server for Chaprola — agent-first data platform. Gives AI agents tools for structured data storage, record CRUD, querying, schema inspection, documentation lookup, web search, URL fetching, scheduled jobs, scoped site keys, and execution via plain HTTP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
package/references/cookbook.md
CHANGED
|
@@ -271,6 +271,103 @@ LET rec = 1
|
|
|
271
271
|
|
|
272
272
|
If the IN-file doesn't exist (e.g., new user), NOT IN treats it as empty — all records pass.
|
|
273
273
|
|
|
274
|
+
## Batch Insert (Multi-Record Append)
|
|
275
|
+
|
|
276
|
+
Insert multiple records in a single call:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
POST /insert-record {
|
|
280
|
+
userid, project, file: "events",
|
|
281
|
+
records: [
|
|
282
|
+
{"event": "login", "ts": "2026-04-09T10:00:00Z"},
|
|
283
|
+
{"event": "purchase", "ts": "2026-04-09T10:05:00Z"},
|
|
284
|
+
{"event": "logout", "ts": "2026-04-09T10:30:00Z"}
|
|
285
|
+
]
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
All-or-nothing: if any record has an invalid field, the entire batch is rejected. Max 1000 per call. Single `record: {}` still works for one record.
|
|
290
|
+
|
|
291
|
+
## UPSERT (Insert or Update)
|
|
292
|
+
|
|
293
|
+
Create if new, update if exists — one call:
|
|
294
|
+
|
|
295
|
+
```bash
|
|
296
|
+
POST /upsert-record {
|
|
297
|
+
userid, project, file: "contacts", key: "contact_id",
|
|
298
|
+
record: {"contact_id": "c-42", "name": "Alice", "status": "active"}
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
If a record with `contact_id = "c-42"` exists, update it. If not, insert it. Batch supported via `records: [...]`.
|
|
303
|
+
|
|
304
|
+
## IN / NOT IN on /query
|
|
305
|
+
|
|
306
|
+
Filter by a set of values:
|
|
307
|
+
|
|
308
|
+
```bash
|
|
309
|
+
POST /query {
|
|
310
|
+
userid, project, file: "orders",
|
|
311
|
+
where: [{"field": "status", "op": "in", "value": ["active", "pending", "review"]}]
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
`not_in` excludes values. Empty array: `in` matches nothing, `not_in` matches everything.
|
|
316
|
+
|
|
317
|
+
## String Transforms on /query
|
|
318
|
+
|
|
319
|
+
Transform field values at query time without a .CS program:
|
|
320
|
+
|
|
321
|
+
```bash
|
|
322
|
+
POST /query {
|
|
323
|
+
userid, project, file: "contacts",
|
|
324
|
+
select: ["email", "name"],
|
|
325
|
+
transform: [{"field": "email", "func": "lower"}, {"field": "name", "func": "trim"}]
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Functions: `upper`, `lower`, `trim`, `substring` (start, length), `replace` (old, new), `length`. Transforms apply before WHERE evaluation.
|
|
330
|
+
|
|
331
|
+
## DISTINCT on /query
|
|
332
|
+
|
|
333
|
+
Return unique rows:
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
POST /query {
|
|
337
|
+
userid, project, file: "products",
|
|
338
|
+
select: ["category"],
|
|
339
|
+
distinct: true
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
Cannot combine with aggregate or pivot.
|
|
344
|
+
|
|
345
|
+
## Date Filters on /query
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
# Records from the last 30 days
|
|
349
|
+
POST /query { where: [{"field": "created_at", "op": "date_within", "value": "30d"}] }
|
|
350
|
+
|
|
351
|
+
# Truncate dates for GROUP BY month (combine with distinct or pivot)
|
|
352
|
+
POST /query { transform: [{"field": "created_at", "func": "date_trunc", "unit": "month"}], distinct: true }
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Units for date_trunc: `year`, `month`, `day`, `hour`. Date fields stored as ISO 8601 strings already sort correctly with `gt`, `lt`, `ge`, `le`.
|
|
356
|
+
|
|
357
|
+
## Parameterized /run
|
|
358
|
+
|
|
359
|
+
Pass named parameters to /run (same as /report supports):
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
POST /run {
|
|
363
|
+
userid, project, name: "PROCESS",
|
|
364
|
+
primary_file: "data",
|
|
365
|
+
params: {"video_id": "V-AT", "action": "advance"}
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Programs read params via `MOVE PARAM.name` or `LET Rn = PARAM.name`. Use /run (not /report) when the program needs to WRITE data.
|
|
370
|
+
|
|
274
371
|
## Async for Large Datasets
|
|
275
372
|
|
|
276
373
|
```bash
|
package/references/gotchas.md
CHANGED
|
@@ -30,16 +30,31 @@ MOVE X.username U.1
|
|
|
30
30
|
GET R41 FROM P.63 10
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
###
|
|
33
|
+
### PRINT concatenation requires the + operator between every term
|
|
34
|
+
The `+` is mandatory. Without it, the compiler rejects the code.
|
|
34
35
|
```chaprola
|
|
35
|
-
//
|
|
36
|
-
PRINT P.name + " earns $" + R41
|
|
37
|
-
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
// CORRECT:
|
|
37
|
+
PRINT "Name: " + P.name + " earns $" + R41
|
|
38
|
+
|
|
39
|
+
// WRONG — compiler error: "requires + between terms"
|
|
40
|
+
PRINT "Name: " P.name
|
|
41
|
+
PRINT P.name " earns $" R41
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
MOVE + PRINT 0 still works but is the old style. Prefer PRINT concatenation.
|
|
45
|
+
|
|
46
|
+
### No commas anywhere in code
|
|
47
|
+
Chaprola has no commas. Not in PRINT, not in function calls, not in lists. Everything is space-separated or uses `+` for concatenation.
|
|
48
|
+
|
|
49
|
+
### No WHILE, FOR, LOOP, CONTINUE, BREAK, SWITCH, CASE
|
|
50
|
+
These keywords do not exist. Use GOTO with labels for all control flow:
|
|
51
|
+
```chaprola
|
|
52
|
+
LET rec = 1
|
|
53
|
+
100 SEEK rec
|
|
54
|
+
IF EOF END
|
|
55
|
+
// ... process ...
|
|
56
|
+
LET rec = rec + 1
|
|
57
|
+
GOTO 100
|
|
43
58
|
```
|
|
44
59
|
|
|
45
60
|
### Use CLEAR, not MOVE BLANKS for full regions
|
|
@@ -57,6 +72,12 @@ MOVE P.txn_type U.76 6
|
|
|
57
72
|
IF EQUAL "CREDIT" U.76 GOTO 200
|
|
58
73
|
```
|
|
59
74
|
|
|
75
|
+
### IF EQUAL "" is not valid — use IF BLANK
|
|
76
|
+
Comparing against an empty string (`IF EQUAL "" U.1 GOTO 200`) is rejected by the compiler. To test if a field is empty, use `IF BLANK`:
|
|
77
|
+
```chaprola
|
|
78
|
+
IF BLANK P.notes GOTO 200 // field is all spaces/empty
|
|
79
|
+
```
|
|
80
|
+
|
|
60
81
|
### MOVE literal auto-pads to field width
|
|
61
82
|
`MOVE "Jones" P.name` auto-fills the rest of the field with blanks. No need to clear first.
|
|
62
83
|
|