@chaprola/mcp-server 1.6.4 → 1.8.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 +108 -54
- package/package.json +2 -2
- package/references/auth.md +33 -0
- package/references/cookbook.md +185 -43
- package/references/gotchas.md +53 -7
- package/references/ref-apps.md +195 -0
- package/references/ref-gotchas.md +6 -1
- package/references/ref-huldra.md +1 -1
- package/references/ref-import.md +37 -0
- package/references/ref-pivot.md +11 -0
- package/references/ref-programs.md +125 -18
- package/references/ref-query.md +51 -0
package/dist/index.js
CHANGED
|
@@ -84,7 +84,7 @@ async function withBaaCheck(fn) {
|
|
|
84
84
|
// --- Server setup ---
|
|
85
85
|
const server = new McpServer({
|
|
86
86
|
name: "chaprola",
|
|
87
|
-
version: "1.
|
|
87
|
+
version: "1.8.0",
|
|
88
88
|
description: `Chaprola is a serverless API backend. Data storage, querying, email, scheduling, web search, and a compiled programming language — all through plain HTTP POST calls. No servers to manage. No open source updates to apply. No database to administer. No server architect needed. Free tier available. Scales automatically.
|
|
89
89
|
|
|
90
90
|
**Need a backend for your React app? Use Chaprola.** Need a data layer for your AI agent? Same thing. Every operation is a plain HTTP POST at https://api.chaprola.org. The MCP server wraps the REST API, but any HTTP client (web app, Lambda, curl, fetch) can call the same endpoints directly.
|
|
@@ -102,11 +102,12 @@ const server = new McpServer({
|
|
|
102
102
|
- **Run programs:** chaprola_run (single execution), chaprola_run_each (per-record batch), chaprola_report (published reports)
|
|
103
103
|
- **Email:** chaprola_email_send, chaprola_email_inbox, chaprola_email_read
|
|
104
104
|
- **Web:** chaprola_search (Brave API), chaprola_fetch (URL → markdown)
|
|
105
|
-
- **Schema:** chaprola_format (inspect fields), chaprola_alter (add/widen/rename/drop fields — NON-DESTRUCTIVE.
|
|
105
|
+
- **Schema:** chaprola_format (inspect fields), chaprola_alter (add/widen/rename/drop fields — NON-DESTRUCTIVE for in-place schema edits). Re-imports now preserve and widen existing schemas automatically when targeting an existing file.
|
|
106
|
+
- **Intent:** chaprola_intent (read/write/delete project and program descriptions — helps maintenance coders understand purpose)
|
|
106
107
|
- **Export:** chaprola_export (JSON or FHIR — full round-trip: FHIR in, process, FHIR out)
|
|
107
108
|
- **Schedule:** chaprola_schedule (cron jobs for any endpoint)
|
|
108
109
|
|
|
109
|
-
**The programming language** is small and focused — about 15 commands. Read chaprola://cookbook before writing source code. Common patterns: aggregation, filtering, scoring, report formatting. Key rules: no PROGRAM keyword, no commas, MOVE
|
|
110
|
+
**The programming language** is small and focused — about 15 commands. Read chaprola://cookbook before writing source code. Common patterns: aggregation, filtering, scoring, report formatting. Key rules: no PROGRAM keyword, no commas, reports can use either the classic MOVE-to-U-buffer then PRINT 0 pattern or one-line PRINT concatenation, and LET supports one operation (no parentheses). Named parameters: PARAM.name reads URL query params as strings; LET x = PARAM.name converts to numeric. Named output positions: U.name instead of U1-U20.
|
|
110
111
|
|
|
111
112
|
**Common misconceptions:**
|
|
112
113
|
- "No JOINs" → Wrong. chaprola_query supports JOIN with hash and merge methods across files. Use chaprola_index to build indexes for fast lookups on join fields.
|
|
@@ -155,7 +156,7 @@ function readRef(filename) {
|
|
|
155
156
|
server.resource("cookbook", "chaprola://cookbook", { description: "Chaprola language cookbook — syntax patterns, complete examples, and the import→compile→run workflow. READ THIS before writing any Chaprola source code.", mimeType: "text/markdown" }, async () => ({
|
|
156
157
|
contents: [{ uri: "chaprola://cookbook", mimeType: "text/markdown", text: readRef("cookbook.md") }],
|
|
157
158
|
}));
|
|
158
|
-
server.resource("gotchas", "chaprola://gotchas", { description: "Common Chaprola mistakes — no parentheses in LET, no commas in PRINT,
|
|
159
|
+
server.resource("gotchas", "chaprola://gotchas", { description: "Common Chaprola mistakes — no parentheses in LET, no commas in PRINT, DEFINE names must not collide with fields, always pass primary_format to compile. READ THIS before writing code.", mimeType: "text/markdown" }, async () => ({
|
|
159
160
|
contents: [{ uri: "chaprola://gotchas", mimeType: "text/markdown", text: readRef("gotchas.md") }],
|
|
160
161
|
}));
|
|
161
162
|
server.resource("endpoints", "chaprola://endpoints", { description: "Chaprola API endpoint reference — all 40 endpoints with request/response shapes", mimeType: "text/markdown" }, async () => ({
|
|
@@ -198,9 +199,12 @@ server.resource("ref-email", "chaprola://ref/email", { description: "Email syste
|
|
|
198
199
|
server.resource("ref-gotchas", "chaprola://ref/gotchas", { description: "Common Chaprola mistakes — language, API, and secondary file pitfalls", mimeType: "text/markdown" }, async () => ({
|
|
199
200
|
contents: [{ uri: "chaprola://ref/gotchas", mimeType: "text/markdown", text: readRef("ref-gotchas.md") }],
|
|
200
201
|
}));
|
|
201
|
-
server.resource("ref-auth", "chaprola://ref/auth", { description: "Authentication details — registration, login, BAA, MCP env vars", mimeType: "text/markdown" }, async () => ({
|
|
202
|
+
server.resource("ref-auth", "chaprola://ref/auth", { description: "Authentication details — registration, login, BAA, cross-user sharing, MCP env vars", mimeType: "text/markdown" }, async () => ({
|
|
202
203
|
contents: [{ uri: "chaprola://ref/auth", mimeType: "text/markdown", text: readRef("ref-auth.md") }],
|
|
203
204
|
}));
|
|
205
|
+
server.resource("ref-apps", "chaprola://ref/apps", { description: "Building apps on Chaprola — React/frontend architecture, site keys, single-owner vs multi-user, enterprise proxy pattern", mimeType: "text/markdown" }, async () => ({
|
|
206
|
+
contents: [{ uri: "chaprola://ref/apps", mimeType: "text/markdown", text: readRef("ref-apps.md") }],
|
|
207
|
+
}));
|
|
204
208
|
// --- MCP Prompts ---
|
|
205
209
|
server.prompt("chaprola-guide", "Essential guide for working with Chaprola. Read this before writing any Chaprola source code.", async () => ({
|
|
206
210
|
messages: [{
|
|
@@ -213,7 +217,7 @@ server.prompt("chaprola-guide", "Essential guide for working with Chaprola. Read
|
|
|
213
217
|
"- NO `PROGRAM` keyword — programs start directly with commands\n" +
|
|
214
218
|
"- NO commas anywhere — all arguments are space-separated\n" +
|
|
215
219
|
"- NO parentheses in LET — only `LET var = a OP b` (one operation)\n" +
|
|
216
|
-
"- Output
|
|
220
|
+
"- Output can use classic MOVE + PRINT 0 buffers or one-line PRINT concatenation (`PRINT \"label\" + P.field + rec`)\n" +
|
|
217
221
|
"- Field addressing: P.fieldname (primary), S.fieldname (secondary)\n" +
|
|
218
222
|
"- Loop pattern: `LET rec = 1` → `SEEK rec` → `IF EOF GOTO end` → process → `LET rec = rec + 1` → `GOTO loop`\n\n" +
|
|
219
223
|
"## Minimal Example\n" +
|
|
@@ -222,10 +226,7 @@ server.prompt("chaprola-guide", "Essential guide for working with Chaprola. Read
|
|
|
222
226
|
"LET rec = 1\n" +
|
|
223
227
|
"100 SEEK rec\n" +
|
|
224
228
|
" IF EOF GOTO 900\n" +
|
|
225
|
-
"
|
|
226
|
-
" MOVE P.name U.1 8\n" +
|
|
227
|
-
" MOVE P.value U.12 6\n" +
|
|
228
|
-
" PRINT 0\n" +
|
|
229
|
+
" PRINT P.name + \" — \" + P.value\n" +
|
|
229
230
|
" LET rec = rec + 1\n" +
|
|
230
231
|
" GOTO 100\n" +
|
|
231
232
|
"900 END\n" +
|
|
@@ -246,6 +247,10 @@ server.tool("chaprola_hello", "Health check — verify the Chaprola API is runni
|
|
|
246
247
|
const res = await fetch(url);
|
|
247
248
|
return textResult(res);
|
|
248
249
|
});
|
|
250
|
+
server.tool("chaprola_help", "Get the full Chaprola documentation bundle from POST /help. Call this before guessing when compile or run fails. No auth required.", {}, async () => {
|
|
251
|
+
const res = await publicFetch("POST", "/help", {});
|
|
252
|
+
return textResult(res);
|
|
253
|
+
});
|
|
249
254
|
server.tool("chaprola_register", "Register a new Chaprola account. Returns an API key — save it immediately", {
|
|
250
255
|
username: z.string().describe("3-40 chars, alphanumeric + hyphens/underscores, starts with letter"),
|
|
251
256
|
passcode: z.string().describe("16-128 characters. Use a long, unique passcode"),
|
|
@@ -334,7 +339,7 @@ server.tool("chaprola_baa_status", "Check whether the authenticated user has sig
|
|
|
334
339
|
return textResult(res);
|
|
335
340
|
});
|
|
336
341
|
// --- Import ---
|
|
337
|
-
server.tool("chaprola_import", "Import JSON data into Chaprola format files (.F + .DA).
|
|
342
|
+
server.tool("chaprola_import", "Import JSON data into Chaprola format files (.F + .DA). If the target file already exists, Chaprola preserves the existing schema, widens matching fields as needed, keeps legacy fields as blanks, and appends new fields at the end. Use chaprola_alter for explicit in-place schema surgery. Sign BAA first if handling PHI", {
|
|
338
343
|
project: z.string().describe("Project name"),
|
|
339
344
|
name: z.string().describe("File name (without extension)"),
|
|
340
345
|
data: z.string().describe("JSON array of record objects to import"),
|
|
@@ -351,7 +356,7 @@ server.tool("chaprola_import", "Import JSON data into Chaprola format files (.F
|
|
|
351
356
|
const res = await authedFetch("/import", body);
|
|
352
357
|
return textResult(res);
|
|
353
358
|
}));
|
|
354
|
-
server.tool("chaprola_import_url", "Get a presigned S3 upload URL for large files (bypasses 6MB API Gateway limit).
|
|
359
|
+
server.tool("chaprola_import_url", "Get a presigned S3 upload URL for large files (bypasses 6MB API Gateway limit). The subsequent chaprola_import_process preserves and widens an existing schema automatically when importing into an existing file.", {
|
|
355
360
|
project: z.string().describe("Project name"),
|
|
356
361
|
name: z.string().describe("File name (without extension)"),
|
|
357
362
|
}, async ({ project, name }) => withBaaCheck(async () => {
|
|
@@ -359,7 +364,7 @@ server.tool("chaprola_import_url", "Get a presigned S3 upload URL for large file
|
|
|
359
364
|
const res = await authedFetch("/import-url", { userid: username, project, name });
|
|
360
365
|
return textResult(res);
|
|
361
366
|
}));
|
|
362
|
-
server.tool("chaprola_import_process", "Process a file previously uploaded to S3 via presigned URL. Generates .F + .DA files.
|
|
367
|
+
server.tool("chaprola_import_process", "Process a file previously uploaded to S3 via presigned URL. Generates .F + .DA files. If the target file already exists, the existing schema is preserved and widened automatically as needed.", {
|
|
363
368
|
project: z.string().describe("Project name"),
|
|
364
369
|
name: z.string().describe("File name (without extension)"),
|
|
365
370
|
format: z.enum(["json", "fhir"]).optional().describe("Data format: json (default) or fhir"),
|
|
@@ -371,7 +376,7 @@ server.tool("chaprola_import_process", "Process a file previously uploaded to S3
|
|
|
371
376
|
const res = await authedFetch("/import-process", body);
|
|
372
377
|
return textResult(res);
|
|
373
378
|
}));
|
|
374
|
-
server.tool("chaprola_import_download", "Import data directly from a public URL (CSV, TSV, JSON, NDJSON, Parquet, Excel). Optional AI-powered schema inference.
|
|
379
|
+
server.tool("chaprola_import_download", "Import data directly from a public URL (CSV, TSV, JSON, NDJSON, Parquet, Excel). Optional AI-powered schema inference.", {
|
|
375
380
|
project: z.string().describe("Project name"),
|
|
376
381
|
name: z.string().describe("Output file name (without extension)"),
|
|
377
382
|
url: z.string().describe("Public URL to download (http/https only)"),
|
|
@@ -391,33 +396,36 @@ server.tool("chaprola_import_download", "Import data directly from a public URL
|
|
|
391
396
|
server.tool("chaprola_export", "Export Chaprola .DA + .F files back to JSON", {
|
|
392
397
|
project: z.string().describe("Project name"),
|
|
393
398
|
name: z.string().describe("File name (without extension)"),
|
|
394
|
-
|
|
399
|
+
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."),
|
|
400
|
+
}, async ({ project, name, userid }) => withBaaCheck(async () => {
|
|
395
401
|
const { username } = getCredentials();
|
|
396
|
-
const res = await authedFetch("/export", { userid: username, project, name });
|
|
402
|
+
const res = await authedFetch("/export", { userid: userid || username, project, name });
|
|
397
403
|
return textResult(res);
|
|
398
404
|
}));
|
|
399
405
|
// --- List ---
|
|
400
406
|
server.tool("chaprola_list", "List files in a project with optional wildcard pattern", {
|
|
401
407
|
project: z.string().describe("Project name (use * for all projects)"),
|
|
402
408
|
pattern: z.string().optional().describe("Wildcard pattern to filter files (e.g., EMP*)"),
|
|
403
|
-
|
|
409
|
+
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."),
|
|
410
|
+
}, async ({ project, pattern, userid }) => withBaaCheck(async () => {
|
|
404
411
|
const { username } = getCredentials();
|
|
405
|
-
const body = { userid: username, project };
|
|
412
|
+
const body = { userid: userid || username, project };
|
|
406
413
|
if (pattern)
|
|
407
414
|
body.pattern = pattern;
|
|
408
415
|
const res = await authedFetch("/list", body);
|
|
409
416
|
return textResult(res);
|
|
410
417
|
}));
|
|
411
418
|
// --- Compile ---
|
|
412
|
-
server.tool("chaprola_compile", "Compile Chaprola source (.CS) to bytecode (.PR). READ chaprola://cookbook BEFORE writing source. Key syntax: no PROGRAM keyword (start with commands), no commas, MOVE+PRINT 0
|
|
419
|
+
server.tool("chaprola_compile", "Compile Chaprola source (.CS) to bytecode (.PR). READ chaprola://cookbook BEFORE writing source. Key syntax: no PROGRAM keyword (start with commands), no commas, reports can use MOVE+PRINT 0 buffers or one-line PRINT concatenation, SEEK for primary records, OPEN/READ/WRITE/CLOSE for secondary files, LET supports one operation (no parentheses). Use primary_format to enable P.fieldname addressing (recommended) — the compiler resolves field names to positions and lengths from the format file. If compile fails, call chaprola_help before retrying.", {
|
|
413
420
|
project: z.string().describe("Project name"),
|
|
414
421
|
name: z.string().describe("Program name (without extension)"),
|
|
415
422
|
source: z.string().describe("Chaprola source code"),
|
|
416
|
-
primary_format: z.string().optional().describe("Primary data file name
|
|
417
|
-
secondary_format: z.string().optional().describe("Secondary
|
|
418
|
-
|
|
423
|
+
primary_format: z.string().optional().describe("Primary data file name — enables P.fieldname addressing (recommended for all programs that reference data fields)"),
|
|
424
|
+
secondary_format: z.string().optional().describe("Secondary data file name — enables S.fieldname addressing (required if using S.fieldname references)"),
|
|
425
|
+
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."),
|
|
426
|
+
}, async ({ project, name, source, primary_format, secondary_format, userid }) => withBaaCheck(async () => {
|
|
419
427
|
const { username } = getCredentials();
|
|
420
|
-
const body = { userid: username, project, name, source };
|
|
428
|
+
const body = { userid: userid || username, project, name, source };
|
|
421
429
|
if (primary_format)
|
|
422
430
|
body.primary_format = primary_format;
|
|
423
431
|
if (secondary_format)
|
|
@@ -426,7 +434,7 @@ server.tool("chaprola_compile", "Compile Chaprola source (.CS) to bytecode (.PR)
|
|
|
426
434
|
return textResult(res);
|
|
427
435
|
}));
|
|
428
436
|
// --- Run ---
|
|
429
|
-
server.tool("chaprola_run", "Execute a compiled .PR program. Use async:true for large datasets (>100K records)", {
|
|
437
|
+
server.tool("chaprola_run", "Execute a compiled .PR program. Use async:true for large datasets (>100K records). If runtime errors occur, call chaprola_help before retrying.", {
|
|
430
438
|
project: z.string().describe("Project name"),
|
|
431
439
|
name: z.string().describe("Program name (without extension)"),
|
|
432
440
|
primary_file: z.string().optional().describe("Primary data file to load"),
|
|
@@ -434,9 +442,10 @@ server.tool("chaprola_run", "Execute a compiled .PR program. Use async:true for
|
|
|
434
442
|
async_exec: z.boolean().optional().describe("If true, run asynchronously and return job_id for polling"),
|
|
435
443
|
secondary_files: z.array(z.string()).optional().describe("Secondary files to make available"),
|
|
436
444
|
nophi: z.boolean().optional().describe("If true, obfuscate PHI-flagged fields during execution"),
|
|
437
|
-
|
|
445
|
+
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."),
|
|
446
|
+
}, async ({ project, name, primary_file, record, async_exec, secondary_files, nophi, userid }) => withBaaCheck(async () => {
|
|
438
447
|
const { username } = getCredentials();
|
|
439
|
-
const body = { userid: username, project, name };
|
|
448
|
+
const body = { userid: userid || username, project, name };
|
|
440
449
|
if (primary_file)
|
|
441
450
|
body.primary_file = primary_file;
|
|
442
451
|
if (record !== undefined)
|
|
@@ -453,9 +462,10 @@ server.tool("chaprola_run", "Execute a compiled .PR program. Use async:true for
|
|
|
453
462
|
server.tool("chaprola_run_status", "Check status of an async job. Returns full output when done", {
|
|
454
463
|
project: z.string().describe("Project name"),
|
|
455
464
|
job_id: z.string().describe("Job ID from async /run response"),
|
|
456
|
-
|
|
465
|
+
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."),
|
|
466
|
+
}, async ({ project, job_id, userid }) => withBaaCheck(async () => {
|
|
457
467
|
const { username } = getCredentials();
|
|
458
|
-
const res = await authedFetch("/run/status", { userid: username, project, job_id });
|
|
468
|
+
const res = await authedFetch("/run/status", { userid: userid || username, project, job_id });
|
|
459
469
|
return textResult(res);
|
|
460
470
|
}));
|
|
461
471
|
server.tool("chaprola_run_each", "Run a compiled .PR program against every record in a data file. Like CHAPRPG from the original SCIOS. Use this for scoring, bulk updates, conditional logic across records.", {
|
|
@@ -468,9 +478,10 @@ server.tool("chaprola_run_each", "Run a compiled .PR program against every recor
|
|
|
468
478
|
value: z.union([z.string(), z.number(), z.array(z.number())]).describe("Value to compare against"),
|
|
469
479
|
})).optional().describe("Optional filter — only run against matching records"),
|
|
470
480
|
where_logic: z.enum(["and", "or"]).optional().describe("How to combine multiple where conditions (default: and)"),
|
|
471
|
-
|
|
481
|
+
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."),
|
|
482
|
+
}, async ({ project, file, program, where, where_logic, userid }) => withBaaCheck(async () => {
|
|
472
483
|
const { username } = getCredentials();
|
|
473
|
-
const body = { userid: username, project, file, program };
|
|
484
|
+
const body = { userid: userid || username, project, file, program };
|
|
474
485
|
if (where)
|
|
475
486
|
body.where = where;
|
|
476
487
|
if (where_logic)
|
|
@@ -478,6 +489,20 @@ server.tool("chaprola_run_each", "Run a compiled .PR program against every recor
|
|
|
478
489
|
const res = await authedFetch("/run-each", body);
|
|
479
490
|
return textResult(res);
|
|
480
491
|
}));
|
|
492
|
+
// --- Systemhelp ---
|
|
493
|
+
server.tool("chaprola_systemhelp", "Send your program name and error message. Chaprola will read your source, intent file, and data schema to diagnose and fix the problem. See POST /help for examples.", {
|
|
494
|
+
project: z.string().describe("Project name"),
|
|
495
|
+
name: z.string().describe("Program name (without extension)"),
|
|
496
|
+
error: z.string().optional().describe("Error message from compile or runtime (copy verbatim if available)"),
|
|
497
|
+
request: z.string().describe("Plain-language description of the problem. Include context: what changed, what you expected, what happened instead."),
|
|
498
|
+
}, async ({ project, name, error, request }) => withBaaCheck(async () => {
|
|
499
|
+
const { username } = getCredentials();
|
|
500
|
+
const body = { userid: username, project, name, request };
|
|
501
|
+
if (error)
|
|
502
|
+
body.error = error;
|
|
503
|
+
const res = await authedFetch("/systemhelp", body);
|
|
504
|
+
return textResult(res);
|
|
505
|
+
}));
|
|
481
506
|
// --- Publish ---
|
|
482
507
|
server.tool("chaprola_publish", "Publish a compiled program for public access via /report", {
|
|
483
508
|
project: z.string().describe("Project name"),
|
|
@@ -514,9 +539,10 @@ server.tool("chaprola_export_report", "Run a .PR program and save output as a pe
|
|
|
514
539
|
format: z.enum(["text", "pdf", "csv", "json", "xlsx"]).optional().describe("Output format (default: text)"),
|
|
515
540
|
title: z.string().optional().describe("Report title (used in PDF header)"),
|
|
516
541
|
nophi: z.boolean().optional().describe("If true, obfuscate PHI-flagged fields"),
|
|
517
|
-
|
|
542
|
+
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."),
|
|
543
|
+
}, async ({ project, name, primary_file, report_name, format, title, nophi, userid }) => withBaaCheck(async () => {
|
|
518
544
|
const { username } = getCredentials();
|
|
519
|
-
const body = { userid: username, project, name };
|
|
545
|
+
const body = { userid: userid || username, project, name };
|
|
520
546
|
if (primary_file)
|
|
521
547
|
body.primary_file = primary_file;
|
|
522
548
|
if (report_name)
|
|
@@ -535,9 +561,10 @@ server.tool("chaprola_download", "Get a presigned S3 URL to download any file yo
|
|
|
535
561
|
project: z.string().describe("Project name"),
|
|
536
562
|
file: z.string().describe("File name with extension (e.g., REPORT.R)"),
|
|
537
563
|
type: z.enum(["data", "format", "source", "proc", "output"]).describe("File type directory"),
|
|
538
|
-
|
|
564
|
+
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."),
|
|
565
|
+
}, async ({ project, file, type, userid }) => withBaaCheck(async () => {
|
|
539
566
|
const { username } = getCredentials();
|
|
540
|
-
const res = await authedFetch("/download", { userid: username, project, file, type });
|
|
567
|
+
const res = await authedFetch("/download", { userid: userid || username, project, file, type });
|
|
541
568
|
return textResult(res);
|
|
542
569
|
}));
|
|
543
570
|
// --- Query ---
|
|
@@ -553,7 +580,8 @@ server.tool("chaprola_query", "SQL-free data query with WHERE, SELECT, aggregati
|
|
|
553
580
|
join: z.string().optional().describe("JSON object of join config, e.g. {\"file\": \"other\", \"on\": \"id\", \"type\": \"inner\"}"),
|
|
554
581
|
pivot: z.string().optional().describe("JSON object of pivot config, e.g. {\"row\": \"category\", \"column\": \"month\", \"values\": \"sales\"}"),
|
|
555
582
|
mercury: z.string().optional().describe("JSON object of mercury scoring config, e.g. {\"fields\": [{\"field\": \"score\", \"target\": 100, \"weight\": 1.0}]}"),
|
|
556
|
-
|
|
583
|
+
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."),
|
|
584
|
+
}, async ({ project, file, where: whereStr, select, aggregate: aggregateStr, order_by: orderByStr, limit, offset, join: joinStr, pivot: pivotStr, mercury: mercuryStr, userid }) => withBaaCheck(async () => {
|
|
557
585
|
const where = typeof whereStr === 'string' ? JSON.parse(whereStr) : whereStr;
|
|
558
586
|
const aggregate = typeof aggregateStr === 'string' ? JSON.parse(aggregateStr) : aggregateStr;
|
|
559
587
|
const order_by = typeof orderByStr === 'string' ? JSON.parse(orderByStr) : orderByStr;
|
|
@@ -561,7 +589,7 @@ server.tool("chaprola_query", "SQL-free data query with WHERE, SELECT, aggregati
|
|
|
561
589
|
const pivot = typeof pivotStr === 'string' ? JSON.parse(pivotStr) : pivotStr;
|
|
562
590
|
const mercury = typeof mercuryStr === 'string' ? JSON.parse(mercuryStr) : mercuryStr;
|
|
563
591
|
const { username } = getCredentials();
|
|
564
|
-
const body = { userid: username, project, file };
|
|
592
|
+
const body = { userid: userid || username, project, file };
|
|
565
593
|
if (where)
|
|
566
594
|
body.where = where;
|
|
567
595
|
if (select)
|
|
@@ -592,9 +620,10 @@ server.tool("chaprola_sort", "Sort a data file by one or more fields. Modifies t
|
|
|
592
620
|
dir: z.enum(["asc", "desc"]).optional(),
|
|
593
621
|
type: z.enum(["text", "numeric"]).optional(),
|
|
594
622
|
})).describe("Sort specification: [{field, dir?, type?}]"),
|
|
595
|
-
|
|
623
|
+
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."),
|
|
624
|
+
}, async ({ project, file, sort_by, userid }) => withBaaCheck(async () => {
|
|
596
625
|
const { username } = getCredentials();
|
|
597
|
-
const res = await authedFetch("/sort", { userid: username, project, file, sort_by });
|
|
626
|
+
const res = await authedFetch("/sort", { userid: userid || username, project, file, sort_by });
|
|
598
627
|
return textResult(res);
|
|
599
628
|
}));
|
|
600
629
|
// --- Index ---
|
|
@@ -602,9 +631,10 @@ server.tool("chaprola_index", "Build an index file (.IDX) for fast lookups on a
|
|
|
602
631
|
project: z.string().describe("Project name"),
|
|
603
632
|
file: z.string().describe("Data file to index"),
|
|
604
633
|
field: z.string().describe("Field name to index"),
|
|
605
|
-
|
|
634
|
+
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."),
|
|
635
|
+
}, async ({ project, file, field, userid }) => withBaaCheck(async () => {
|
|
606
636
|
const { username } = getCredentials();
|
|
607
|
-
const res = await authedFetch("/index", { userid: username, project, file, field });
|
|
637
|
+
const res = await authedFetch("/index", { userid: userid || username, project, file, field });
|
|
608
638
|
return textResult(res);
|
|
609
639
|
}));
|
|
610
640
|
// --- Merge ---
|
|
@@ -614,13 +644,14 @@ server.tool("chaprola_merge", "Merge two sorted data files into one. Both must s
|
|
|
614
644
|
file_b: z.string().describe("Second data file"),
|
|
615
645
|
output: z.string().describe("Output file name"),
|
|
616
646
|
key: z.string().describe("Merge key field"),
|
|
617
|
-
|
|
647
|
+
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."),
|
|
648
|
+
}, async ({ project, file_a, file_b, output, key, userid }) => withBaaCheck(async () => {
|
|
618
649
|
const { username } = getCredentials();
|
|
619
|
-
const res = await authedFetch("/merge", { userid: username, project, file_a, file_b, output, key });
|
|
650
|
+
const res = await authedFetch("/merge", { userid: userid || username, project, file_a, file_b, output, key });
|
|
620
651
|
return textResult(res);
|
|
621
652
|
}));
|
|
622
653
|
// --- Schema: Format + Alter ---
|
|
623
|
-
server.tool("chaprola_format", "Inspect a data file's schema — returns field names,
|
|
654
|
+
server.tool("chaprola_format", "Inspect a data file's schema — returns field names, types, and PHI flags. Use this to understand the data structure before writing programs.", {
|
|
624
655
|
project: z.string().describe("Project name"),
|
|
625
656
|
name: z.string().describe("Data file name (without .F extension)"),
|
|
626
657
|
}, async ({ project, name }) => withBaaCheck(async () => {
|
|
@@ -628,7 +659,7 @@ server.tool("chaprola_format", "Inspect a data file's schema — returns field n
|
|
|
628
659
|
const res = await authedFetch("/format", { userid: username, project, name });
|
|
629
660
|
return textResult(res);
|
|
630
661
|
}));
|
|
631
|
-
server.tool("chaprola_alter", "Modify a data file's schema: widen/narrow/rename fields, add new fields, drop fields. NON-DESTRUCTIVE: Transforms existing data to match the new schema.
|
|
662
|
+
server.tool("chaprola_alter", "Modify a data file's schema: widen/narrow/rename fields, add new fields, drop fields. NON-DESTRUCTIVE: Transforms existing data to match the new schema. Use this for explicit schema surgery on existing data files; re-imports now preserve and widen existing schemas automatically, but chaprola_alter still handles rename/drop/narrow operations.", {
|
|
632
663
|
project: z.string().describe("Project name"),
|
|
633
664
|
name: z.string().describe("Data file name (without extension)"),
|
|
634
665
|
alter: z.array(z.object({
|
|
@@ -659,6 +690,24 @@ server.tool("chaprola_alter", "Modify a data file's schema: widen/narrow/rename
|
|
|
659
690
|
const res = await authedFetch("/alter", body);
|
|
660
691
|
return textResult(res);
|
|
661
692
|
}));
|
|
693
|
+
// --- Intent ---
|
|
694
|
+
server.tool("chaprola_intent", "Read, write, or delete project and program intent descriptions. Project intent describes the purpose of a project. Program intent (.DS file) describes what a specific program does. Omit 'text' and 'delete' to read. Provide 'text' to write. Set 'delete: true' to remove.", {
|
|
695
|
+
project: z.string().describe("Project name"),
|
|
696
|
+
name: z.string().optional().describe("Program name (without extension). Omit for project-level intent"),
|
|
697
|
+
text: z.string().optional().describe("Intent text to write. Omit to read current intent"),
|
|
698
|
+
delete: z.boolean().optional().describe("Set true to delete the intent"),
|
|
699
|
+
}, async ({ project, name, text, delete: del }) => {
|
|
700
|
+
const { username } = getCredentials();
|
|
701
|
+
const body = { userid: username, project };
|
|
702
|
+
if (name)
|
|
703
|
+
body.name = name;
|
|
704
|
+
if (text !== undefined)
|
|
705
|
+
body.text = text;
|
|
706
|
+
if (del)
|
|
707
|
+
body.delete = true;
|
|
708
|
+
const res = await authedFetch("/intent", body);
|
|
709
|
+
return textResult(res);
|
|
710
|
+
});
|
|
662
711
|
// --- Optimize (HULDRA) ---
|
|
663
712
|
server.tool("chaprola_optimize", "Run HULDRA nonlinear optimization using a compiled .PR as the objective evaluator", {
|
|
664
713
|
project: z.string().describe("Project name"),
|
|
@@ -809,10 +858,11 @@ server.tool("chaprola_insert_record", "Insert a new record into a data file's me
|
|
|
809
858
|
project: z.string().describe("Project name"),
|
|
810
859
|
file: z.string().describe("Data file name (without extension)"),
|
|
811
860
|
record: z.string().describe("JSON object of the record to insert, e.g. {\"name\": \"foo\", \"status\": \"active\"}. Unspecified fields default to blanks."),
|
|
812
|
-
|
|
861
|
+
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."),
|
|
862
|
+
}, async ({ project, file, record: recordStr, userid }) => withBaaCheck(async () => {
|
|
813
863
|
const record = typeof recordStr === 'string' ? JSON.parse(recordStr) : recordStr;
|
|
814
864
|
const { username } = getCredentials();
|
|
815
|
-
const res = await authedFetch("/insert-record", { userid: username, project, file, record });
|
|
865
|
+
const res = await authedFetch("/insert-record", { userid: userid || username, project, file, record });
|
|
816
866
|
return textResult(res);
|
|
817
867
|
}));
|
|
818
868
|
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.", {
|
|
@@ -820,38 +870,42 @@ server.tool("chaprola_update_record", "Update fields in a single record matched
|
|
|
820
870
|
file: z.string().describe("Data file name (without extension)"),
|
|
821
871
|
where: z.string().describe("JSON object of filter conditions for which records to update, e.g. {\"id\": \"123\"}"),
|
|
822
872
|
set: z.string().describe("JSON object of fields to update, e.g. {\"status\": \"done\"}"),
|
|
823
|
-
|
|
873
|
+
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."),
|
|
874
|
+
}, async ({ project, file, where: whereStr, set: setStr, userid }) => withBaaCheck(async () => {
|
|
824
875
|
const whereClause = typeof whereStr === 'string' ? JSON.parse(whereStr) : whereStr;
|
|
825
876
|
const set = typeof setStr === 'string' ? JSON.parse(setStr) : setStr;
|
|
826
877
|
const { username } = getCredentials();
|
|
827
|
-
const res = await authedFetch("/update-record", { userid: username, project, file, where: whereClause, set });
|
|
878
|
+
const res = await authedFetch("/update-record", { userid: userid || username, project, file, where: whereClause, set });
|
|
828
879
|
return textResult(res);
|
|
829
880
|
}));
|
|
830
881
|
server.tool("chaprola_delete_record", "Delete a single record matched by a where clause. Marks the record as ignored (.IGN). Physically removed on consolidation.", {
|
|
831
882
|
project: z.string().describe("Project name"),
|
|
832
883
|
file: z.string().describe("Data file name (without extension)"),
|
|
833
884
|
where: z.string().describe("JSON object of filter conditions for which records to delete, e.g. {\"id\": \"123\"}"),
|
|
834
|
-
|
|
885
|
+
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."),
|
|
886
|
+
}, async ({ project, file, where: whereStr, userid }) => withBaaCheck(async () => {
|
|
835
887
|
const whereClause = typeof whereStr === 'string' ? JSON.parse(whereStr) : whereStr;
|
|
836
888
|
const { username } = getCredentials();
|
|
837
|
-
const res = await authedFetch("/delete-record", { userid: username, project, file, where: whereClause });
|
|
889
|
+
const res = await authedFetch("/delete-record", { userid: userid || username, project, file, where: whereClause });
|
|
838
890
|
return textResult(res);
|
|
839
891
|
}));
|
|
840
892
|
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.", {
|
|
841
893
|
project: z.string().describe("Project name"),
|
|
842
894
|
file: z.string().describe("Data file name (without extension)"),
|
|
843
|
-
|
|
895
|
+
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."),
|
|
896
|
+
}, async ({ project, file, userid }) => withBaaCheck(async () => {
|
|
844
897
|
const { username } = getCredentials();
|
|
845
|
-
const res = await authedFetch("/consolidate", { userid: username, project, file });
|
|
898
|
+
const res = await authedFetch("/consolidate", { userid: userid || username, project, file });
|
|
846
899
|
return textResult(res);
|
|
847
900
|
}));
|
|
848
901
|
// --- Challenge (Data Health) ---
|
|
849
902
|
server.tool("chaprola_challenge", "Data health check: finds missing data, overdue dates, and incomplete records. Returns issues sorted by severity.", {
|
|
850
903
|
project: z.string().describe("Project name"),
|
|
851
904
|
file: z.string().describe("Data file name (without extension)"),
|
|
852
|
-
|
|
905
|
+
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."),
|
|
906
|
+
}, async ({ project, file, userid }) => withBaaCheck(async () => {
|
|
853
907
|
const { username } = getCredentials();
|
|
854
|
-
const res = await authedFetch("/challenge", { userid: username, project, file });
|
|
908
|
+
const res = await authedFetch("/challenge", { userid: userid || username, project, file });
|
|
855
909
|
return textResult(res);
|
|
856
910
|
}));
|
|
857
911
|
// --- Site Keys ---
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chaprola/mcp-server",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "MCP server for Chaprola — agent-first data platform. Gives AI agents
|
|
3
|
+
"version": "1.8.0",
|
|
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",
|
|
7
7
|
"bin": {
|
package/references/auth.md
CHANGED
|
@@ -51,6 +51,39 @@ If you do handle PHI, sign the BAA once per account:
|
|
|
51
51
|
|
|
52
52
|
These are read by the MCP server and injected into every authenticated request automatically.
|
|
53
53
|
|
|
54
|
+
## Cross-User Project Sharing
|
|
55
|
+
|
|
56
|
+
By default, only the project owner can read and write their data. To grant another user access, create an `access.json` file in the project root:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
s3://chaprola-2026/{owner}/{project}/access.json
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Format:**
|
|
63
|
+
```json
|
|
64
|
+
{
|
|
65
|
+
"owner": "tawni",
|
|
66
|
+
"project": "social",
|
|
67
|
+
"discoverable": false,
|
|
68
|
+
"description": "Content calendar and social media",
|
|
69
|
+
"writers": [
|
|
70
|
+
{"username": "cal", "granted_at": "2026-04-06T17:25:16Z"},
|
|
71
|
+
{"username": "nora", "granted_at": "2026-04-06T17:25:16Z"}
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**How it works:**
|
|
77
|
+
- Any user in the `writers` list gets full read+write access to that project through all API endpoints (query, update-record, insert-record, delete-record, export, compile, run, run-each, etc.)
|
|
78
|
+
- The shared user passes the **owner's** userid in requests: `{"userid": "tawni", "project": "social", ...}` — authenticated with their own API key
|
|
79
|
+
- Every protected endpoint checks: (1) does the API key's username match the `userid`? If not, (2) is the user listed as a writer in `access.json`?
|
|
80
|
+
- No `access.json` = no shared access (owner-only)
|
|
81
|
+
- `discoverable` is reserved for future use (project discovery/listing)
|
|
82
|
+
|
|
83
|
+
**To set up sharing:** Create or update the `access.json` file via S3 directly or through the `/sv` admin interface.
|
|
84
|
+
|
|
85
|
+
**To revoke access:** Remove the user from the `writers` array, or delete `access.json` entirely.
|
|
86
|
+
|
|
54
87
|
## Credential Recovery
|
|
55
88
|
|
|
56
89
|
If your API key stops working (403):
|