@chaprola/mcp-server 1.7.0 → 1.9.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.
@@ -103,6 +103,7 @@ const server = new McpServer({
103
103
  - **Email:** chaprola_email_send, chaprola_email_inbox, chaprola_email_read
104
104
  - **Web:** chaprola_search (Brave API), chaprola_fetch (URL → markdown)
105
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
 
@@ -395,33 +396,50 @@ server.tool("chaprola_import_download", "Import data directly from a public URL
395
396
  server.tool("chaprola_export", "Export Chaprola .DA + .F files back to JSON", {
396
397
  project: z.string().describe("Project name"),
397
398
  name: z.string().describe("File name (without extension)"),
398
- }, 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 () => {
399
401
  const { username } = getCredentials();
400
- const res = await authedFetch("/export", { userid: username, project, name });
402
+ const res = await authedFetch("/export", { userid: userid || username, project, name });
401
403
  return textResult(res);
402
404
  }));
403
405
  // --- List ---
404
406
  server.tool("chaprola_list", "List files in a project with optional wildcard pattern", {
405
407
  project: z.string().describe("Project name (use * for all projects)"),
406
408
  pattern: z.string().optional().describe("Wildcard pattern to filter files (e.g., EMP*)"),
407
- }, 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 () => {
408
411
  const { username } = getCredentials();
409
- const body = { userid: username, project };
412
+ const body = { userid: userid || username, project };
410
413
  if (pattern)
411
414
  body.pattern = pattern;
412
415
  const res = await authedFetch("/list", body);
413
416
  return textResult(res);
414
417
  }));
415
418
  // --- Compile ---
416
- 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.", {
419
+ server.tool("chaprola_compile", `Compile Chaprola source (.CS) to bytecode (.PR). READ chaprola://cookbook BEFORE writing source.
420
+
421
+ STYLE RULES (mandatory — project review enforces these):
422
+ 1. Use QUERY instead of SEEK loops for filtering or single-record lookup. SEEK loops only for processing every record unconditionally.
423
+ 2. Don't use MOVE + IF EQUAL for comparisons — use QUERY WHERE.
424
+ 3. Use implicit variable assignment (LET name = value) — don't use DEFINE VARIABLE.
425
+ 4. END/STOP only for early exit — not needed at end of program.
426
+ 5. OPEN PRIMARY not needed when using QUERY with primary_format.
427
+ 6. Use named read (READ name rec + name.field) instead of OPEN SECONDARY + S.field for QUERY results.
428
+ 7. Every program MUST have an intent file (.DS) — one paragraph: what the program does, parameters, output, who uses it.
429
+ 8. Add a comment header — first lines describe purpose and parameters.
430
+ 9. Use PRINT concatenation (PRINT "text" + P.field + R1), not MOVE + PRINT 0 buffers.
431
+ 10. Use RECORDNUMBERS for bulk delete: QUERY INTO name, then DELETE PRIMARY name.RECORDNUMBERS.
432
+
433
+ KEY SYNTAX: no PROGRAM keyword (start with commands), no commas, LET supports one operation (no parentheses), no built-in functions. Use primary_format to enable P.fieldname addressing — the compiler resolves field names to positions and lengths from the format file. If compile fails, call chaprola_help before retrying.`, {
417
434
  project: z.string().describe("Project name"),
418
435
  name: z.string().describe("Program name (without extension)"),
419
436
  source: z.string().describe("Chaprola source code"),
420
437
  primary_format: z.string().optional().describe("Primary data file name — enables P.fieldname addressing (recommended for all programs that reference data fields)"),
421
438
  secondary_format: z.string().optional().describe("Secondary data file name — enables S.fieldname addressing (required if using S.fieldname references)"),
422
- }, async ({ project, name, source, primary_format, secondary_format }) => withBaaCheck(async () => {
439
+ 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."),
440
+ }, async ({ project, name, source, primary_format, secondary_format, userid }) => withBaaCheck(async () => {
423
441
  const { username } = getCredentials();
424
- const body = { userid: username, project, name, source };
442
+ const body = { userid: userid || username, project, name, source };
425
443
  if (primary_format)
426
444
  body.primary_format = primary_format;
427
445
  if (secondary_format)
@@ -438,9 +456,10 @@ server.tool("chaprola_run", "Execute a compiled .PR program. Use async:true for
438
456
  async_exec: z.boolean().optional().describe("If true, run asynchronously and return job_id for polling"),
439
457
  secondary_files: z.array(z.string()).optional().describe("Secondary files to make available"),
440
458
  nophi: z.boolean().optional().describe("If true, obfuscate PHI-flagged fields during execution"),
441
- }, async ({ project, name, primary_file, record, async_exec, secondary_files, nophi }) => withBaaCheck(async () => {
459
+ 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."),
460
+ }, async ({ project, name, primary_file, record, async_exec, secondary_files, nophi, userid }) => withBaaCheck(async () => {
442
461
  const { username } = getCredentials();
443
- const body = { userid: username, project, name };
462
+ const body = { userid: userid || username, project, name };
444
463
  if (primary_file)
445
464
  body.primary_file = primary_file;
446
465
  if (record !== undefined)
@@ -457,9 +476,10 @@ server.tool("chaprola_run", "Execute a compiled .PR program. Use async:true for
457
476
  server.tool("chaprola_run_status", "Check status of an async job. Returns full output when done", {
458
477
  project: z.string().describe("Project name"),
459
478
  job_id: z.string().describe("Job ID from async /run response"),
460
- }, async ({ project, job_id }) => withBaaCheck(async () => {
479
+ 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."),
480
+ }, async ({ project, job_id, userid }) => withBaaCheck(async () => {
461
481
  const { username } = getCredentials();
462
- const res = await authedFetch("/run/status", { userid: username, project, job_id });
482
+ const res = await authedFetch("/run/status", { userid: userid || username, project, job_id });
463
483
  return textResult(res);
464
484
  }));
465
485
  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.", {
@@ -472,9 +492,10 @@ server.tool("chaprola_run_each", "Run a compiled .PR program against every recor
472
492
  value: z.union([z.string(), z.number(), z.array(z.number())]).describe("Value to compare against"),
473
493
  })).optional().describe("Optional filter — only run against matching records"),
474
494
  where_logic: z.enum(["and", "or"]).optional().describe("How to combine multiple where conditions (default: and)"),
475
- }, async ({ project, file, program, where, where_logic }) => withBaaCheck(async () => {
495
+ 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."),
496
+ }, async ({ project, file, program, where, where_logic, userid }) => withBaaCheck(async () => {
476
497
  const { username } = getCredentials();
477
- const body = { userid: username, project, file, program };
498
+ const body = { userid: userid || username, project, file, program };
478
499
  if (where)
479
500
  body.where = where;
480
501
  if (where_logic)
@@ -532,9 +553,10 @@ server.tool("chaprola_export_report", "Run a .PR program and save output as a pe
532
553
  format: z.enum(["text", "pdf", "csv", "json", "xlsx"]).optional().describe("Output format (default: text)"),
533
554
  title: z.string().optional().describe("Report title (used in PDF header)"),
534
555
  nophi: z.boolean().optional().describe("If true, obfuscate PHI-flagged fields"),
535
- }, async ({ project, name, primary_file, report_name, format, title, nophi }) => withBaaCheck(async () => {
556
+ 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."),
557
+ }, async ({ project, name, primary_file, report_name, format, title, nophi, userid }) => withBaaCheck(async () => {
536
558
  const { username } = getCredentials();
537
- const body = { userid: username, project, name };
559
+ const body = { userid: userid || username, project, name };
538
560
  if (primary_file)
539
561
  body.primary_file = primary_file;
540
562
  if (report_name)
@@ -553,9 +575,10 @@ server.tool("chaprola_download", "Get a presigned S3 URL to download any file yo
553
575
  project: z.string().describe("Project name"),
554
576
  file: z.string().describe("File name with extension (e.g., REPORT.R)"),
555
577
  type: z.enum(["data", "format", "source", "proc", "output"]).describe("File type directory"),
556
- }, async ({ project, file, type }) => withBaaCheck(async () => {
578
+ 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."),
579
+ }, async ({ project, file, type, userid }) => withBaaCheck(async () => {
557
580
  const { username } = getCredentials();
558
- const res = await authedFetch("/download", { userid: username, project, file, type });
581
+ const res = await authedFetch("/download", { userid: userid || username, project, file, type });
559
582
  return textResult(res);
560
583
  }));
561
584
  // --- Query ---
@@ -571,7 +594,8 @@ server.tool("chaprola_query", "SQL-free data query with WHERE, SELECT, aggregati
571
594
  join: z.string().optional().describe("JSON object of join config, e.g. {\"file\": \"other\", \"on\": \"id\", \"type\": \"inner\"}"),
572
595
  pivot: z.string().optional().describe("JSON object of pivot config, e.g. {\"row\": \"category\", \"column\": \"month\", \"values\": \"sales\"}"),
573
596
  mercury: z.string().optional().describe("JSON object of mercury scoring config, e.g. {\"fields\": [{\"field\": \"score\", \"target\": 100, \"weight\": 1.0}]}"),
574
- }, async ({ project, file, where: whereStr, select, aggregate: aggregateStr, order_by: orderByStr, limit, offset, join: joinStr, pivot: pivotStr, mercury: mercuryStr }) => withBaaCheck(async () => {
597
+ 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."),
598
+ }, async ({ project, file, where: whereStr, select, aggregate: aggregateStr, order_by: orderByStr, limit, offset, join: joinStr, pivot: pivotStr, mercury: mercuryStr, userid }) => withBaaCheck(async () => {
575
599
  const where = typeof whereStr === 'string' ? JSON.parse(whereStr) : whereStr;
576
600
  const aggregate = typeof aggregateStr === 'string' ? JSON.parse(aggregateStr) : aggregateStr;
577
601
  const order_by = typeof orderByStr === 'string' ? JSON.parse(orderByStr) : orderByStr;
@@ -579,7 +603,7 @@ server.tool("chaprola_query", "SQL-free data query with WHERE, SELECT, aggregati
579
603
  const pivot = typeof pivotStr === 'string' ? JSON.parse(pivotStr) : pivotStr;
580
604
  const mercury = typeof mercuryStr === 'string' ? JSON.parse(mercuryStr) : mercuryStr;
581
605
  const { username } = getCredentials();
582
- const body = { userid: username, project, file };
606
+ const body = { userid: userid || username, project, file };
583
607
  if (where)
584
608
  body.where = where;
585
609
  if (select)
@@ -610,9 +634,10 @@ server.tool("chaprola_sort", "Sort a data file by one or more fields. Modifies t
610
634
  dir: z.enum(["asc", "desc"]).optional(),
611
635
  type: z.enum(["text", "numeric"]).optional(),
612
636
  })).describe("Sort specification: [{field, dir?, type?}]"),
613
- }, async ({ project, file, sort_by }) => withBaaCheck(async () => {
637
+ 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."),
638
+ }, async ({ project, file, sort_by, userid }) => withBaaCheck(async () => {
614
639
  const { username } = getCredentials();
615
- const res = await authedFetch("/sort", { userid: username, project, file, sort_by });
640
+ const res = await authedFetch("/sort", { userid: userid || username, project, file, sort_by });
616
641
  return textResult(res);
617
642
  }));
618
643
  // --- Index ---
@@ -620,9 +645,10 @@ server.tool("chaprola_index", "Build an index file (.IDX) for fast lookups on a
620
645
  project: z.string().describe("Project name"),
621
646
  file: z.string().describe("Data file to index"),
622
647
  field: z.string().describe("Field name to index"),
623
- }, async ({ project, file, field }) => withBaaCheck(async () => {
648
+ 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."),
649
+ }, async ({ project, file, field, userid }) => withBaaCheck(async () => {
624
650
  const { username } = getCredentials();
625
- const res = await authedFetch("/index", { userid: username, project, file, field });
651
+ const res = await authedFetch("/index", { userid: userid || username, project, file, field });
626
652
  return textResult(res);
627
653
  }));
628
654
  // --- Merge ---
@@ -632,9 +658,10 @@ server.tool("chaprola_merge", "Merge two sorted data files into one. Both must s
632
658
  file_b: z.string().describe("Second data file"),
633
659
  output: z.string().describe("Output file name"),
634
660
  key: z.string().describe("Merge key field"),
635
- }, async ({ project, file_a, file_b, output, key }) => withBaaCheck(async () => {
661
+ 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."),
662
+ }, async ({ project, file_a, file_b, output, key, userid }) => withBaaCheck(async () => {
636
663
  const { username } = getCredentials();
637
- const res = await authedFetch("/merge", { userid: username, project, file_a, file_b, output, key });
664
+ const res = await authedFetch("/merge", { userid: userid || username, project, file_a, file_b, output, key });
638
665
  return textResult(res);
639
666
  }));
640
667
  // --- Schema: Format + Alter ---
@@ -677,6 +704,24 @@ server.tool("chaprola_alter", "Modify a data file's schema: widen/narrow/rename
677
704
  const res = await authedFetch("/alter", body);
678
705
  return textResult(res);
679
706
  }));
707
+ // --- Intent ---
708
+ 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.", {
709
+ project: z.string().describe("Project name"),
710
+ name: z.string().optional().describe("Program name (without extension). Omit for project-level intent"),
711
+ text: z.string().optional().describe("Intent text to write. Omit to read current intent"),
712
+ delete: z.boolean().optional().describe("Set true to delete the intent"),
713
+ }, async ({ project, name, text, delete: del }) => {
714
+ const { username } = getCredentials();
715
+ const body = { userid: username, project };
716
+ if (name)
717
+ body.name = name;
718
+ if (text !== undefined)
719
+ body.text = text;
720
+ if (del)
721
+ body.delete = true;
722
+ const res = await authedFetch("/intent", body);
723
+ return textResult(res);
724
+ });
680
725
  // --- Optimize (HULDRA) ---
681
726
  server.tool("chaprola_optimize", "Run HULDRA nonlinear optimization using a compiled .PR as the objective evaluator", {
682
727
  project: z.string().describe("Project name"),
@@ -827,10 +872,11 @@ server.tool("chaprola_insert_record", "Insert a new record into a data file's me
827
872
  project: z.string().describe("Project name"),
828
873
  file: z.string().describe("Data file name (without extension)"),
829
874
  record: z.string().describe("JSON object of the record to insert, e.g. {\"name\": \"foo\", \"status\": \"active\"}. Unspecified fields default to blanks."),
830
- }, async ({ project, file, record: recordStr }) => withBaaCheck(async () => {
875
+ 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."),
876
+ }, async ({ project, file, record: recordStr, userid }) => withBaaCheck(async () => {
831
877
  const record = typeof recordStr === 'string' ? JSON.parse(recordStr) : recordStr;
832
878
  const { username } = getCredentials();
833
- const res = await authedFetch("/insert-record", { userid: username, project, file, record });
879
+ const res = await authedFetch("/insert-record", { userid: userid || username, project, file, record });
834
880
  return textResult(res);
835
881
  }));
836
882
  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.", {
@@ -838,38 +884,42 @@ server.tool("chaprola_update_record", "Update fields in a single record matched
838
884
  file: z.string().describe("Data file name (without extension)"),
839
885
  where: z.string().describe("JSON object of filter conditions for which records to update, e.g. {\"id\": \"123\"}"),
840
886
  set: z.string().describe("JSON object of fields to update, e.g. {\"status\": \"done\"}"),
841
- }, async ({ project, file, where: whereStr, set: setStr }) => withBaaCheck(async () => {
887
+ 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."),
888
+ }, async ({ project, file, where: whereStr, set: setStr, userid }) => withBaaCheck(async () => {
842
889
  const whereClause = typeof whereStr === 'string' ? JSON.parse(whereStr) : whereStr;
843
890
  const set = typeof setStr === 'string' ? JSON.parse(setStr) : setStr;
844
891
  const { username } = getCredentials();
845
- const res = await authedFetch("/update-record", { userid: username, project, file, where: whereClause, set });
892
+ const res = await authedFetch("/update-record", { userid: userid || username, project, file, where: whereClause, set });
846
893
  return textResult(res);
847
894
  }));
848
895
  server.tool("chaprola_delete_record", "Delete a single record matched by a where clause. Marks the record as ignored (.IGN). Physically removed on consolidation.", {
849
896
  project: z.string().describe("Project name"),
850
897
  file: z.string().describe("Data file name (without extension)"),
851
898
  where: z.string().describe("JSON object of filter conditions for which records to delete, e.g. {\"id\": \"123\"}"),
852
- }, async ({ project, file, where: whereStr }) => withBaaCheck(async () => {
899
+ 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."),
900
+ }, async ({ project, file, where: whereStr, userid }) => withBaaCheck(async () => {
853
901
  const whereClause = typeof whereStr === 'string' ? JSON.parse(whereStr) : whereStr;
854
902
  const { username } = getCredentials();
855
- const res = await authedFetch("/delete-record", { userid: username, project, file, where: whereClause });
903
+ const res = await authedFetch("/delete-record", { userid: userid || username, project, file, where: whereClause });
856
904
  return textResult(res);
857
905
  }));
858
906
  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.", {
859
907
  project: z.string().describe("Project name"),
860
908
  file: z.string().describe("Data file name (without extension)"),
861
- }, async ({ project, file }) => withBaaCheck(async () => {
909
+ 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."),
910
+ }, async ({ project, file, userid }) => withBaaCheck(async () => {
862
911
  const { username } = getCredentials();
863
- const res = await authedFetch("/consolidate", { userid: username, project, file });
912
+ const res = await authedFetch("/consolidate", { userid: userid || username, project, file });
864
913
  return textResult(res);
865
914
  }));
866
915
  // --- Challenge (Data Health) ---
867
916
  server.tool("chaprola_challenge", "Data health check: finds missing data, overdue dates, and incomplete records. Returns issues sorted by severity.", {
868
917
  project: z.string().describe("Project name"),
869
918
  file: z.string().describe("Data file name (without extension)"),
870
- }, async ({ project, file }) => withBaaCheck(async () => {
919
+ 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."),
920
+ }, async ({ project, file, userid }) => withBaaCheck(async () => {
871
921
  const { username } = getCredentials();
872
- const res = await authedFetch("/challenge", { userid: username, project, file });
922
+ const res = await authedFetch("/challenge", { userid: userid || username, project, file });
873
923
  return textResult(res);
874
924
  }));
875
925
  // --- Site Keys ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chaprola/mcp-server",
3
- "version": "1.7.0",
3
+ "version": "1.9.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",