@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 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.4.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. Use this, NOT chaprola_import, to modify schemas on existing data.)
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+PRINT 0 buffer model, 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
+ **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, MOVE length must match field width, DEFINE names must not collide with fields. READ THIS before writing code.", mimeType: "text/markdown" }, async () => ({
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 uses MOVE + PRINT 0 buffer model, NOT `PRINT field`\n" +
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
- " MOVE BLANKS U.1 40\n" +
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). DESTRUCTIVE: This REPLACES both the format (.F) and data (.DA) files if they already exist. All existing data will be lost. Use chaprola_alter to modify field widths/schema on existing data. Use chaprola_import only for new data or when replacing entire datasets. Sign BAA first if handling PHI", {
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). DESTRUCTIVE: The subsequent chaprola_import_process will replace existing data. Use chaprola_alter to modify schemas on existing data.", {
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. DESTRUCTIVE: Replaces existing data if the file already exists. Use chaprola_alter to modify schemas on existing data.", {
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. DESTRUCTIVE: Replaces existing data if the file already exists. Use chaprola_alter to modify schemas on existing data.", {
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
- }, async ({ project, name }) => withBaaCheck(async () => {
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
- }, async ({ project, pattern }) => withBaaCheck(async () => {
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 buffer model (not PRINT field), SEEK for primary records, OPEN/READ/WRITE/CLOSE for secondary files, LET supports one operation (no parentheses), field addressing via P.field/S.field requires primary_format/secondary_format params.", {
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 (enables P.fieldname addressing)"),
417
- secondary_format: z.string().optional().describe("Secondary format file name (enables S.fieldname addressing)"),
418
- }, async ({ project, name, source, primary_format, secondary_format }) => withBaaCheck(async () => {
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
- }, async ({ project, name, primary_file, record, async_exec, secondary_files, nophi }) => withBaaCheck(async () => {
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
- }, async ({ project, job_id }) => withBaaCheck(async () => {
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
- }, async ({ project, file, program, where, where_logic }) => withBaaCheck(async () => {
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
- }, async ({ project, name, primary_file, report_name, format, title, nophi }) => withBaaCheck(async () => {
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
- }, async ({ project, file, type }) => withBaaCheck(async () => {
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
- }, async ({ project, file, where: whereStr, select, aggregate: aggregateStr, order_by: orderByStr, limit, offset, join: joinStr, pivot: pivotStr, mercury: mercuryStr }) => withBaaCheck(async () => {
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
- }, async ({ project, file, sort_by }) => withBaaCheck(async () => {
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
- }, async ({ project, file, field }) => withBaaCheck(async () => {
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
- }, async ({ project, file_a, file_b, output, key }) => withBaaCheck(async () => {
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, positions, lengths, types, and PHI flags", {
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. This is the ONLY safe way to change field widths or schema on existing data files. Unlike chaprola_import which replaces all data, chaprola_alter preserves and reformats existing records.", {
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
- }, async ({ project, file, record: recordStr }) => withBaaCheck(async () => {
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
- }, async ({ project, file, where: whereStr, set: setStr }) => withBaaCheck(async () => {
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
- }, async ({ project, file, where: whereStr }) => withBaaCheck(async () => {
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
- }, async ({ project, file }) => withBaaCheck(async () => {
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
- }, async ({ project, file }) => withBaaCheck(async () => {
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.6.4",
4
- "description": "MCP server for Chaprola — agent-first data platform. Gives AI agents 47 tools for structured data storage, record CRUD, querying, schema inspection, web search, URL fetching, scheduled jobs, and execution via plain HTTP.",
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": {
@@ -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):