@chaprola/mcp-server 1.0.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/README.md ADDED
@@ -0,0 +1,144 @@
1
+ # @chaprola/mcp-server
2
+
3
+ MCP server for [Chaprola](https://chaprola.org) — the agent-first data platform.
4
+
5
+ Gives AI agents 35 tools for structured data storage, querying, compilation, and execution through the [Model Context Protocol](https://modelcontextprotocol.io).
6
+
7
+ ## Quick Start
8
+
9
+ ### Claude Code
10
+
11
+ ```bash
12
+ claude mcp add chaprola-mcp -e CHAPROLA_USERNAME=yourusername -e CHAPROLA_API_KEY=chp_yourkey -- npx @chaprola/mcp-server
13
+ ```
14
+
15
+ ### Claude Desktop
16
+
17
+ Add to `claude_desktop_config.json`:
18
+
19
+ ```json
20
+ {
21
+ "mcpServers": {
22
+ "chaprola": {
23
+ "command": "npx",
24
+ "args": ["@chaprola/mcp-server"],
25
+ "env": {
26
+ "CHAPROLA_USERNAME": "yourusername",
27
+ "CHAPROLA_API_KEY": "chp_yourkey"
28
+ }
29
+ }
30
+ }
31
+ }
32
+ ```
33
+
34
+ ### VS Code / Copilot
35
+
36
+ Add to `.vscode/mcp.json`:
37
+
38
+ ```json
39
+ {
40
+ "servers": {
41
+ "chaprola": {
42
+ "command": "npx",
43
+ "args": ["@chaprola/mcp-server"],
44
+ "env": {
45
+ "CHAPROLA_USERNAME": "yourusername",
46
+ "CHAPROLA_API_KEY": "chp_yourkey"
47
+ }
48
+ }
49
+ }
50
+ }
51
+ ```
52
+
53
+ ### Cursor
54
+
55
+ Add to `.cursor/mcp.json`:
56
+
57
+ ```json
58
+ {
59
+ "mcpServers": {
60
+ "chaprola": {
61
+ "command": "npx",
62
+ "args": ["@chaprola/mcp-server"],
63
+ "env": {
64
+ "CHAPROLA_USERNAME": "yourusername",
65
+ "CHAPROLA_API_KEY": "chp_yourkey"
66
+ }
67
+ }
68
+ }
69
+ }
70
+ ```
71
+
72
+ ## Getting Credentials
73
+
74
+ ```bash
75
+ # Register (returns your API key — save it immediately)
76
+ curl -X POST https://api.chaprola.org/register \
77
+ -H "Content-Type: application/json" \
78
+ -d '{"username": "myname", "passcode": "my-secure-passcode-16chars"}'
79
+ ```
80
+
81
+ Or use the `chaprola_register` tool after connecting.
82
+
83
+ ## Available Tools
84
+
85
+ | Tool | Description |
86
+ |------|-------------|
87
+ | `chaprola_hello` | Health check |
88
+ | `chaprola_register` | Create account |
89
+ | `chaprola_login` | Login (get new API key) |
90
+ | `chaprola_check_username` | Check username availability |
91
+ | `chaprola_delete_account` | Delete account + all data |
92
+ | `chaprola_sign_baa` | Sign Business Associate Agreement (PHI only) |
93
+ | `chaprola_baa_status` | Check BAA status |
94
+ | `chaprola_baa_text` | Get BAA text |
95
+ | `chaprola_import` | Import JSON to Chaprola format |
96
+ | `chaprola_import_url` | Get presigned upload URL |
97
+ | `chaprola_import_process` | Process uploaded file |
98
+ | `chaprola_import_download` | Import from URL (CSV/Excel/JSON/Parquet) |
99
+ | `chaprola_export` | Export to JSON |
100
+ | `chaprola_list` | List files |
101
+ | `chaprola_compile` | Compile .CS source to .PR bytecode |
102
+ | `chaprola_run` | Execute .PR program |
103
+ | `chaprola_run_status` | Check async job status |
104
+ | `chaprola_publish` | Publish program for public access |
105
+ | `chaprola_unpublish` | Remove public access |
106
+ | `chaprola_report` | Run published program (no auth) |
107
+ | `chaprola_export_report` | Run program and save output |
108
+ | `chaprola_download` | Get presigned download URL |
109
+ | `chaprola_query` | Filter, aggregate, join data |
110
+ | `chaprola_sort` | Sort data file |
111
+ | `chaprola_index` | Build index on field |
112
+ | `chaprola_merge` | Merge two sorted files |
113
+ | `chaprola_optimize` | HULDRA nonlinear optimization |
114
+ | `chaprola_optimize_status` | Check optimization status |
115
+ | `chaprola_email_inbox` | List emails |
116
+ | `chaprola_email_read` | Read email |
117
+ | `chaprola_email_send` | Send email |
118
+ | `chaprola_email_delete` | Delete email |
119
+
120
+ ## Resources
121
+
122
+ The server exposes reference documentation as MCP resources:
123
+
124
+ - `chaprola://cookbook` — Language cookbook with complete examples
125
+ - `chaprola://endpoints` — All 35 API endpoints
126
+ - `chaprola://auth` — Authentication reference
127
+ - `chaprola://gotchas` — Common mistakes to avoid
128
+
129
+ ## Environment Variables
130
+
131
+ | Variable | Required | Description |
132
+ |----------|----------|-------------|
133
+ | `CHAPROLA_USERNAME` | Yes | Your registered username |
134
+ | `CHAPROLA_API_KEY` | Yes | Your API key (format: `chp_` + 64 hex chars) |
135
+
136
+ ## HIPAA / BAA
137
+
138
+ Non-PHI data works without a signed BAA. If handling Protected Health Information (PHI), a human must review and sign the BAA first. The server includes guardrails that warn agents when the BAA is not signed.
139
+
140
+ ## Links
141
+
142
+ - Website: [chaprola.org](https://chaprola.org)
143
+ - API: [api.chaprola.org](https://api.chaprola.org/hello)
144
+ - Status: [UptimeRobot](https://stats.uptimerobot.com/1gkN4Tx0RX)
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,587 @@
1
+ #!/usr/bin/env node
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { z } from "zod";
5
+ const BASE_URL = "https://api.chaprola.org";
6
+ // --- Auth helper ---
7
+ function getCredentials() {
8
+ const username = process.env.CHAPROLA_USERNAME;
9
+ const apiKey = process.env.CHAPROLA_API_KEY;
10
+ if (!username || !apiKey) {
11
+ throw new Error("CHAPROLA_USERNAME and CHAPROLA_API_KEY environment variables are required. " +
12
+ "Register at POST https://api.chaprola.org/register to get an API key.");
13
+ }
14
+ return { username, apiKey };
15
+ }
16
+ async function authedFetch(path, body) {
17
+ const { apiKey } = getCredentials();
18
+ return fetch(`${BASE_URL}${path}`, {
19
+ method: "POST",
20
+ headers: {
21
+ "Content-Type": "application/json",
22
+ "Authorization": `Bearer ${apiKey}`,
23
+ },
24
+ body: JSON.stringify(body),
25
+ });
26
+ }
27
+ async function publicFetch(method, path, body) {
28
+ const opts = {
29
+ method,
30
+ headers: { "Content-Type": "application/json" },
31
+ };
32
+ if (body && method === "POST") {
33
+ opts.body = JSON.stringify(body);
34
+ }
35
+ return fetch(`${BASE_URL}${path}`, opts);
36
+ }
37
+ async function textResult(res) {
38
+ const text = await res.text();
39
+ return { content: [{ type: "text", text }] };
40
+ }
41
+ // --- BAA guardrail ---
42
+ let baaStatusCache = null;
43
+ const BAA_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
44
+ async function checkBaaStatus() {
45
+ const now = Date.now();
46
+ if (baaStatusCache && (now - baaStatusCache.checkedAt) < BAA_CACHE_TTL_MS) {
47
+ return baaStatusCache.signed;
48
+ }
49
+ try {
50
+ const { username } = getCredentials();
51
+ const res = await authedFetch("/baa-status", { userid: username });
52
+ const data = await res.json();
53
+ const signed = data.signed === true;
54
+ baaStatusCache = { signed, checkedAt: now };
55
+ return signed;
56
+ }
57
+ catch {
58
+ // Fail open — let the server enforce
59
+ return true;
60
+ }
61
+ }
62
+ async function baaGuard() {
63
+ const signed = await checkBaaStatus();
64
+ if (!signed) {
65
+ return ("NOTE: BAA not signed. Non-PHI data operations will work normally without a BAA.\n" +
66
+ "If this data contains Protected Health Information (PHI), a human MUST review and\n" +
67
+ "approve the BAA before proceeding. DO NOT sign the BAA automatically — a human\n" +
68
+ "must explicitly confirm after reading the full BAA text.\n" +
69
+ "To sign: call chaprola_baa_text, show the full text to the human, and only call\n" +
70
+ "chaprola_sign_baa after they explicitly approve.");
71
+ }
72
+ return null;
73
+ }
74
+ async function withBaaCheck(fn) {
75
+ const warning = await baaGuard();
76
+ const result = await fn();
77
+ if (warning) {
78
+ // Prepend warning to the result
79
+ const existingText = result.content[0]?.text || "";
80
+ return { content: [{ type: "text", text: warning + "\n\n---\n\n" + existingText }] };
81
+ }
82
+ return result;
83
+ }
84
+ // --- Server setup ---
85
+ const server = new McpServer({
86
+ name: "chaprola",
87
+ version: "1.0.0",
88
+ });
89
+ // --- MCP Resources (language reference for agents) ---
90
+ import { readFileSync } from "fs";
91
+ import { fileURLToPath } from "url";
92
+ import { dirname, join } from "path";
93
+ const __dirname = dirname(fileURLToPath(import.meta.url));
94
+ // Try multiple paths: installed package (dist/references/), dev mode (../../references/)
95
+ function findRefsDir() {
96
+ const candidates = [
97
+ join(__dirname, "..", "references"), // installed: dist/../references/
98
+ join(__dirname, "..", "..", "references"), // dev (tsx): src/../../references/
99
+ ];
100
+ for (const dir of candidates) {
101
+ try {
102
+ readFileSync(join(dir, "cookbook.md"), "utf-8");
103
+ return dir;
104
+ }
105
+ catch { /* try next */ }
106
+ }
107
+ return candidates[0]; // fallback
108
+ }
109
+ const refsDir = findRefsDir();
110
+ function readRef(filename) {
111
+ try {
112
+ return readFileSync(join(refsDir, filename), "utf-8");
113
+ }
114
+ catch {
115
+ return `(Could not load ${filename})`;
116
+ }
117
+ }
118
+ 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 () => ({
119
+ contents: [{ uri: "chaprola://cookbook", mimeType: "text/markdown", text: readRef("cookbook.md") }],
120
+ }));
121
+ 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 () => ({
122
+ contents: [{ uri: "chaprola://gotchas", mimeType: "text/markdown", text: readRef("gotchas.md") }],
123
+ }));
124
+ server.resource("endpoints", "chaprola://endpoints", { description: "Chaprola API endpoint reference — all 35 endpoints with request/response shapes", mimeType: "text/markdown" }, async () => ({
125
+ contents: [{ uri: "chaprola://endpoints", mimeType: "text/markdown", text: readRef("endpoints.md") }],
126
+ }));
127
+ server.resource("auth", "chaprola://auth", { description: "Chaprola authentication reference — API key model, BAA flow, credential recovery", mimeType: "text/markdown" }, async () => ({
128
+ contents: [{ uri: "chaprola://auth", mimeType: "text/markdown", text: readRef("auth.md") }],
129
+ }));
130
+ // --- MCP Prompts ---
131
+ server.prompt("chaprola-guide", "Essential guide for working with Chaprola. Read this before writing any Chaprola source code.", async () => ({
132
+ messages: [{
133
+ role: "user",
134
+ content: {
135
+ type: "text",
136
+ text: "# Chaprola Quick Reference\n\n" +
137
+ "Chaprola is NOT a general-purpose language. Key differences:\n\n" +
138
+ "## Syntax Rules\n" +
139
+ "- NO `PROGRAM` keyword — programs start directly with commands\n" +
140
+ "- NO commas anywhere — all arguments are space-separated\n" +
141
+ "- NO parentheses in LET — only `LET var = a OP b` (one operation)\n" +
142
+ "- Output uses MOVE + PRINT 0 buffer model, NOT `PRINT field`\n" +
143
+ "- Field addressing: P.fieldname (primary), S.fieldname (secondary)\n" +
144
+ "- Loop pattern: `LET rec = 1` → `SEEK rec` → `IF EOF GOTO end` → process → `LET rec = rec + 1` → `GOTO loop`\n\n" +
145
+ "## Minimal Example\n" +
146
+ "```\n" +
147
+ "DEFINE VARIABLE rec R1\n" +
148
+ "LET rec = 1\n" +
149
+ "100 SEEK rec\n" +
150
+ " IF EOF GOTO 900\n" +
151
+ " MOVE BLANKS U.1 40\n" +
152
+ " MOVE P.name U.1 8\n" +
153
+ " MOVE P.value U.12 6\n" +
154
+ " PRINT 0\n" +
155
+ " LET rec = rec + 1\n" +
156
+ " GOTO 100\n" +
157
+ "900 END\n" +
158
+ "```\n\n" +
159
+ "## BAA Policy\n" +
160
+ "- Non-PHI data works WITHOUT a signed BAA\n" +
161
+ "- NEVER sign the BAA automatically — a human must read and explicitly approve\n" +
162
+ "- Only needed when handling Protected Health Information (PHI)\n\n" +
163
+ "Read chaprola://cookbook and chaprola://gotchas for full reference.",
164
+ },
165
+ }],
166
+ }));
167
+ // ============================================================
168
+ // PUBLIC ENDPOINTS (no auth required)
169
+ // ============================================================
170
+ server.tool("chaprola_hello", "Health check — verify the Chaprola API is running", { name: z.string().optional().describe("Name to greet (default: world)") }, async ({ name }) => {
171
+ const url = name ? `${BASE_URL}/hello?name=${encodeURIComponent(name)}` : `${BASE_URL}/hello`;
172
+ const res = await fetch(url);
173
+ return textResult(res);
174
+ });
175
+ server.tool("chaprola_register", "Register a new Chaprola account. Returns an API key — save it immediately", {
176
+ username: z.string().describe("3-40 chars, alphanumeric + hyphens/underscores, starts with letter"),
177
+ passcode: z.string().describe("16-128 characters. Use a long, unique passcode"),
178
+ }, async ({ username, passcode }) => {
179
+ const res = await publicFetch("POST", "/register", { username, passcode });
180
+ return textResult(res);
181
+ });
182
+ server.tool("chaprola_login", "Login and get a new API key. WARNING: invalidates the previous API key", {
183
+ username: z.string().describe("Your registered username"),
184
+ passcode: z.string().describe("Your passcode"),
185
+ }, async ({ username, passcode }) => {
186
+ const res = await publicFetch("POST", "/login", { username, passcode });
187
+ return textResult(res);
188
+ });
189
+ server.tool("chaprola_check_username", "Check if a username is available before registering", { username: z.string().describe("Username to check") }, async ({ username }) => {
190
+ const res = await publicFetch("POST", "/check-username", { username });
191
+ return textResult(res);
192
+ });
193
+ server.tool("chaprola_delete_account", "Delete an account and all associated data. Requires passcode confirmation", {
194
+ username: z.string().describe("Account username to delete"),
195
+ passcode: z.string().describe("Account passcode for confirmation"),
196
+ }, async ({ username, passcode }) => {
197
+ const res = await publicFetch("POST", "/delete-account", { username, passcode });
198
+ return textResult(res);
199
+ });
200
+ server.tool("chaprola_baa_text", "Get the current Business Associate Agreement text and version. Present to human for review before signing", {}, async () => {
201
+ const res = await publicFetch("POST", "/baa-text", {});
202
+ return textResult(res);
203
+ });
204
+ server.tool("chaprola_report", "Run a published program and return output. No auth required — program must be published first", {
205
+ userid: z.string().describe("Owner of the published program"),
206
+ project: z.string().describe("Project containing the program"),
207
+ name: z.string().describe("Name of the published .PR file"),
208
+ primary_file: z.string().optional().describe("Data file to load"),
209
+ }, async ({ userid, project, name, primary_file }) => {
210
+ const body = { userid, project, name };
211
+ if (primary_file)
212
+ body.primary_file = primary_file;
213
+ const res = await publicFetch("POST", "/report", body);
214
+ return textResult(res);
215
+ });
216
+ // ============================================================
217
+ // AUTHENTICATED ENDPOINTS
218
+ // ============================================================
219
+ // --- BAA ---
220
+ server.tool("chaprola_sign_baa", "Sign the BAA. STOP: You MUST call chaprola_baa_text first, show the FULL text to the human, and get their EXPLICIT typed approval before calling this. Never sign automatically. Only needed for PHI — non-PHI data works without a BAA.", {
221
+ signatory_name: z.string().describe("Full name of the person agreeing to the BAA"),
222
+ signatory_title: z.string().optional().describe("Title of the signatory"),
223
+ organization: z.string().optional().describe("Organization name (the Covered Entity)"),
224
+ }, async ({ signatory_name, signatory_title, organization }) => {
225
+ const { username } = getCredentials();
226
+ const body = { userid: username, signatory_name };
227
+ if (signatory_title)
228
+ body.signatory_title = signatory_title;
229
+ if (organization)
230
+ body.organization = organization;
231
+ const res = await authedFetch("/sign-baa", body);
232
+ return textResult(res);
233
+ });
234
+ server.tool("chaprola_baa_status", "Check whether the authenticated user has signed the BAA", {}, async () => {
235
+ const { username } = getCredentials();
236
+ const res = await authedFetch("/baa-status", { userid: username });
237
+ return textResult(res);
238
+ });
239
+ // --- Import ---
240
+ server.tool("chaprola_import", "Import JSON data into Chaprola format files (.F + .DA). Sign BAA first if handling PHI", {
241
+ project: z.string().describe("Project name"),
242
+ name: z.string().describe("File name (without extension)"),
243
+ data: z.array(z.record(z.any())).describe("Array of flat JSON objects to import"),
244
+ format: z.enum(["json", "fhir"]).optional().describe("Data format: json (default) or fhir"),
245
+ expires_in_days: z.number().optional().describe("Days until data expires (default: 90)"),
246
+ }, async ({ project, name, data, format, expires_in_days }) => withBaaCheck(async () => {
247
+ const { username } = getCredentials();
248
+ const body = { userid: username, project, name, data };
249
+ if (format)
250
+ body.format = format;
251
+ if (expires_in_days)
252
+ body.expires_in_days = expires_in_days;
253
+ const res = await authedFetch("/import", body);
254
+ return textResult(res);
255
+ }));
256
+ server.tool("chaprola_import_url", "Get a presigned S3 upload URL for large files (bypasses 6MB API Gateway limit)", {
257
+ project: z.string().describe("Project name"),
258
+ name: z.string().describe("File name (without extension)"),
259
+ }, async ({ project, name }) => withBaaCheck(async () => {
260
+ const { username } = getCredentials();
261
+ const res = await authedFetch("/import-url", { userid: username, project, name });
262
+ return textResult(res);
263
+ }));
264
+ server.tool("chaprola_import_process", "Process a file previously uploaded to S3 via presigned URL. Generates .F + .DA files", {
265
+ project: z.string().describe("Project name"),
266
+ name: z.string().describe("File name (without extension)"),
267
+ format: z.enum(["json", "fhir"]).optional().describe("Data format: json (default) or fhir"),
268
+ }, async ({ project, name, format }) => withBaaCheck(async () => {
269
+ const { username } = getCredentials();
270
+ const body = { userid: username, project, name };
271
+ if (format)
272
+ body.format = format;
273
+ const res = await authedFetch("/import-process", body);
274
+ return textResult(res);
275
+ }));
276
+ server.tool("chaprola_import_download", "Import data directly from a public URL (CSV, TSV, JSON, NDJSON, Parquet, Excel). Optional AI-powered schema inference", {
277
+ project: z.string().describe("Project name"),
278
+ name: z.string().describe("Output file name (without extension)"),
279
+ url: z.string().url().describe("Public URL to download (http/https only)"),
280
+ instructions: z.string().optional().describe("Natural language instructions for AI-powered field selection and transforms"),
281
+ max_rows: z.number().optional().describe("Maximum rows to import (default: 5,000,000)"),
282
+ }, async ({ project, name, url, instructions, max_rows }) => withBaaCheck(async () => {
283
+ const { username } = getCredentials();
284
+ const body = { userid: username, project, name, url };
285
+ if (instructions)
286
+ body.instructions = instructions;
287
+ if (max_rows)
288
+ body.max_rows = max_rows;
289
+ const res = await authedFetch("/import-download", body);
290
+ return textResult(res);
291
+ }));
292
+ // --- Export ---
293
+ server.tool("chaprola_export", "Export Chaprola .DA + .F files back to JSON", {
294
+ project: z.string().describe("Project name"),
295
+ name: z.string().describe("File name (without extension)"),
296
+ }, async ({ project, name }) => withBaaCheck(async () => {
297
+ const { username } = getCredentials();
298
+ const res = await authedFetch("/export", { userid: username, project, name });
299
+ return textResult(res);
300
+ }));
301
+ // --- List ---
302
+ server.tool("chaprola_list", "List files in a project with optional wildcard pattern", {
303
+ project: z.string().describe("Project name (use * for all projects)"),
304
+ pattern: z.string().optional().describe("Wildcard pattern to filter files (e.g., EMP*)"),
305
+ }, async ({ project, pattern }) => withBaaCheck(async () => {
306
+ const { username } = getCredentials();
307
+ const body = { userid: username, project };
308
+ if (pattern)
309
+ body.pattern = pattern;
310
+ const res = await authedFetch("/list", body);
311
+ return textResult(res);
312
+ }));
313
+ // --- Compile ---
314
+ 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_formats params.", {
315
+ project: z.string().describe("Project name"),
316
+ name: z.string().describe("Program name (without extension)"),
317
+ source: z.string().describe("Chaprola source code"),
318
+ primary_format: z.string().optional().describe("Primary data file name (enables P.fieldname addressing)"),
319
+ secondary_formats: z.array(z.string()).optional().describe("Secondary format file names (enables S.fieldname addressing)"),
320
+ }, async ({ project, name, source, primary_format, secondary_formats }) => withBaaCheck(async () => {
321
+ const { username } = getCredentials();
322
+ const body = { userid: username, project, name, source };
323
+ if (primary_format)
324
+ body.primary_format = primary_format;
325
+ if (secondary_formats)
326
+ body.secondary_formats = secondary_formats;
327
+ const res = await authedFetch("/compile", body);
328
+ return textResult(res);
329
+ }));
330
+ // --- Run ---
331
+ server.tool("chaprola_run", "Execute a compiled .PR program. Use async:true for large datasets (>100K records)", {
332
+ project: z.string().describe("Project name"),
333
+ name: z.string().describe("Program name (without extension)"),
334
+ primary_file: z.string().optional().describe("Primary data file to load"),
335
+ record: z.number().optional().describe("Starting record number"),
336
+ async_exec: z.boolean().optional().describe("If true, run asynchronously and return job_id for polling"),
337
+ secondary_files: z.array(z.string()).optional().describe("Secondary files to make available"),
338
+ nophi: z.boolean().optional().describe("If true, obfuscate PHI-flagged fields during execution"),
339
+ }, async ({ project, name, primary_file, record, async_exec, secondary_files, nophi }) => withBaaCheck(async () => {
340
+ const { username } = getCredentials();
341
+ const body = { userid: username, project, name };
342
+ if (primary_file)
343
+ body.primary_file = primary_file;
344
+ if (record !== undefined)
345
+ body.record = record;
346
+ if (async_exec !== undefined)
347
+ body.async = async_exec;
348
+ if (secondary_files)
349
+ body.secondary_files = secondary_files;
350
+ if (nophi !== undefined)
351
+ body.nophi = nophi;
352
+ const res = await authedFetch("/run", body);
353
+ return textResult(res);
354
+ }));
355
+ server.tool("chaprola_run_status", "Check status of an async job. Returns full output when done", {
356
+ project: z.string().describe("Project name"),
357
+ job_id: z.string().describe("Job ID from async /run response"),
358
+ }, async ({ project, job_id }) => withBaaCheck(async () => {
359
+ const { username } = getCredentials();
360
+ const res = await authedFetch("/run/status", { userid: username, project, job_id });
361
+ return textResult(res);
362
+ }));
363
+ // --- Publish ---
364
+ server.tool("chaprola_publish", "Publish a compiled program for public access via /report", {
365
+ project: z.string().describe("Project name"),
366
+ name: z.string().describe("Program name to publish"),
367
+ primary_file: z.string().optional().describe("Data file to load when running the report"),
368
+ record: z.number().optional().describe("Starting record number"),
369
+ }, async ({ project, name, primary_file, record }) => withBaaCheck(async () => {
370
+ const { username } = getCredentials();
371
+ const body = { userid: username, project, name };
372
+ if (primary_file)
373
+ body.primary_file = primary_file;
374
+ if (record !== undefined)
375
+ body.record = record;
376
+ const res = await authedFetch("/publish", body);
377
+ return textResult(res);
378
+ }));
379
+ server.tool("chaprola_unpublish", "Remove public access from a published program", {
380
+ project: z.string().describe("Project name"),
381
+ name: z.string().describe("Program name to unpublish"),
382
+ }, async ({ project, name }) => withBaaCheck(async () => {
383
+ const { username } = getCredentials();
384
+ const res = await authedFetch("/unpublish", { userid: username, project, name });
385
+ return textResult(res);
386
+ }));
387
+ // --- Export Report ---
388
+ server.tool("chaprola_export_report", "Run a .PR program and save output as a persistent .R file in S3", {
389
+ project: z.string().describe("Project name"),
390
+ name: z.string().describe("Program name"),
391
+ primary_file: z.string().optional().describe("Primary data file to load"),
392
+ report_name: z.string().optional().describe("Custom output file name"),
393
+ format: z.enum(["text", "pdf", "csv", "json", "xlsx"]).optional().describe("Output format (default: text)"),
394
+ title: z.string().optional().describe("Report title (used in PDF header)"),
395
+ nophi: z.boolean().optional().describe("If true, obfuscate PHI-flagged fields"),
396
+ }, async ({ project, name, primary_file, report_name, format, title, nophi }) => withBaaCheck(async () => {
397
+ const { username } = getCredentials();
398
+ const body = { userid: username, project, name };
399
+ if (primary_file)
400
+ body.primary_file = primary_file;
401
+ if (report_name)
402
+ body.report_name = report_name;
403
+ if (format)
404
+ body.format = format;
405
+ if (title)
406
+ body.title = title;
407
+ if (nophi !== undefined)
408
+ body.nophi = nophi;
409
+ const res = await authedFetch("/export-report", body);
410
+ return textResult(res);
411
+ }));
412
+ // --- Download ---
413
+ server.tool("chaprola_download", "Get a presigned S3 URL to download any file you own (1-hour expiry)", {
414
+ project: z.string().describe("Project name"),
415
+ file: z.string().describe("File name with extension (e.g., REPORT.R)"),
416
+ type: z.enum(["data", "format", "source", "proc", "output"]).describe("File type directory"),
417
+ }, async ({ project, file, type }) => withBaaCheck(async () => {
418
+ const { username } = getCredentials();
419
+ const res = await authedFetch("/download", { userid: username, project, file, type });
420
+ return textResult(res);
421
+ }));
422
+ // --- Query ---
423
+ server.tool("chaprola_query", "SQL-free data query with WHERE, SELECT, aggregation, ORDER BY, JOIN, pivot, and Mercury scoring", {
424
+ project: z.string().describe("Project name"),
425
+ file: z.string().describe("Data file to query"),
426
+ where: z.record(z.any()).optional().describe("Filter: {field, op, value}. Ops: eq, ne, gt, ge, lt, le, between, contains, starts_with"),
427
+ select: z.array(z.string()).optional().describe("Fields to include in output"),
428
+ aggregate: z.array(z.record(z.any())).optional().describe("Aggregation: [{field, func}]. Funcs: count, sum, avg, min, max, stddev"),
429
+ order_by: z.array(z.record(z.any())).optional().describe("Sort: [{field, dir}]"),
430
+ limit: z.number().optional().describe("Max results to return"),
431
+ offset: z.number().optional().describe("Skip this many results"),
432
+ join: z.record(z.any()).optional().describe("Join: {file, on, type, method}"),
433
+ pivot: z.record(z.any()).optional().describe("Pivot: {row, column, values, totals, grand_total}"),
434
+ mercury: z.record(z.any()).optional().describe("Mercury scoring: {fields: [{field, target, weight}]}"),
435
+ }, async ({ project, file, where, select, aggregate, order_by, limit, offset, join, pivot, mercury }) => withBaaCheck(async () => {
436
+ const { username } = getCredentials();
437
+ const body = { userid: username, project, file };
438
+ if (where)
439
+ body.where = where;
440
+ if (select)
441
+ body.select = select;
442
+ if (aggregate)
443
+ body.aggregate = aggregate;
444
+ if (order_by)
445
+ body.order_by = order_by;
446
+ if (limit !== undefined)
447
+ body.limit = limit;
448
+ if (offset !== undefined)
449
+ body.offset = offset;
450
+ if (join)
451
+ body.join = join;
452
+ if (pivot)
453
+ body.pivot = pivot;
454
+ if (mercury)
455
+ body.mercury = mercury;
456
+ const res = await authedFetch("/query", body);
457
+ return textResult(res);
458
+ }));
459
+ // --- Sort ---
460
+ server.tool("chaprola_sort", "Sort a data file by one or more fields. Modifies the file in place", {
461
+ project: z.string().describe("Project name"),
462
+ file: z.string().describe("Data file to sort"),
463
+ sort_by: z.array(z.object({
464
+ field: z.string(),
465
+ dir: z.enum(["asc", "desc"]).optional(),
466
+ type: z.enum(["text", "numeric"]).optional(),
467
+ })).describe("Sort specification: [{field, dir?, type?}]"),
468
+ }, async ({ project, file, sort_by }) => withBaaCheck(async () => {
469
+ const { username } = getCredentials();
470
+ const res = await authedFetch("/sort", { userid: username, project, file, sort_by });
471
+ return textResult(res);
472
+ }));
473
+ // --- Index ---
474
+ server.tool("chaprola_index", "Build an index file (.IDX) for fast lookups on a field", {
475
+ project: z.string().describe("Project name"),
476
+ file: z.string().describe("Data file to index"),
477
+ field: z.string().describe("Field name to index"),
478
+ }, async ({ project, file, field }) => withBaaCheck(async () => {
479
+ const { username } = getCredentials();
480
+ const res = await authedFetch("/index", { userid: username, project, file, field });
481
+ return textResult(res);
482
+ }));
483
+ // --- Merge ---
484
+ server.tool("chaprola_merge", "Merge two sorted data files into one. Both must share the same format (.F)", {
485
+ project: z.string().describe("Project name"),
486
+ file_a: z.string().describe("First data file"),
487
+ file_b: z.string().describe("Second data file"),
488
+ output: z.string().describe("Output file name"),
489
+ key: z.string().describe("Merge key field"),
490
+ }, async ({ project, file_a, file_b, output, key }) => withBaaCheck(async () => {
491
+ const { username } = getCredentials();
492
+ const res = await authedFetch("/merge", { userid: username, project, file_a, file_b, output, key });
493
+ return textResult(res);
494
+ }));
495
+ // --- Optimize (HULDRA) ---
496
+ server.tool("chaprola_optimize", "Run HULDRA nonlinear optimization using a compiled .PR as the objective evaluator", {
497
+ project: z.string().describe("Project name"),
498
+ program: z.string().describe("Compiled .PR program name (the VALUE program)"),
499
+ primary_file: z.string().describe("Data file to pass to the VALUE program"),
500
+ elements: z.array(z.object({
501
+ index: z.number().describe("R-variable index (1-20)"),
502
+ label: z.string(),
503
+ start: z.number(),
504
+ min: z.number(),
505
+ max: z.number(),
506
+ delta: z.number(),
507
+ })).describe("Parameters to optimize"),
508
+ objectives: z.array(z.object({
509
+ index: z.number().describe("R-variable index (1-20) — maps to R(20+index)"),
510
+ label: z.string(),
511
+ goal: z.number(),
512
+ weight: z.number(),
513
+ })).describe("Objective values to minimize"),
514
+ max_iterations: z.number().optional().describe("Max iterations (default: 100)"),
515
+ h_initial: z.number().optional().describe("Initial step fraction (default: 0.125)"),
516
+ async_exec: z.boolean().optional().describe("If true, return job_id for long optimizations"),
517
+ }, async ({ project, program, primary_file, elements, objectives, max_iterations, h_initial, async_exec }) => withBaaCheck(async () => {
518
+ const { username } = getCredentials();
519
+ const body = { userid: username, project, program, primary_file, elements, objectives };
520
+ if (max_iterations !== undefined)
521
+ body.max_iterations = max_iterations;
522
+ if (h_initial !== undefined)
523
+ body.h_initial = h_initial;
524
+ if (async_exec !== undefined)
525
+ body.async = async_exec;
526
+ const res = await authedFetch("/optimize", body);
527
+ return textResult(res);
528
+ }));
529
+ server.tool("chaprola_optimize_status", "Check status of an async optimization job", {
530
+ project: z.string().describe("Project name"),
531
+ job_id: z.string().describe("Job ID from async /optimize response"),
532
+ }, async ({ project, job_id }) => withBaaCheck(async () => {
533
+ const { username } = getCredentials();
534
+ const res = await authedFetch("/optimize/status", { userid: username, project, job_id });
535
+ return textResult(res);
536
+ }));
537
+ // --- Email ---
538
+ server.tool("chaprola_email_inbox", "List emails in the authenticated user's mailbox", {
539
+ limit: z.number().optional().describe("Max emails to return (default 20, max 100)"),
540
+ before: z.string().optional().describe("ISO 8601 timestamp — return emails before this time"),
541
+ }, async ({ limit, before }) => withBaaCheck(async () => {
542
+ const { username } = getCredentials();
543
+ const body = { address: username };
544
+ if (limit !== undefined)
545
+ body.limit = limit;
546
+ if (before)
547
+ body.before = before;
548
+ const res = await authedFetch("/email/inbox", body);
549
+ return textResult(res);
550
+ }));
551
+ server.tool("chaprola_email_read", "Read a specific email by message_id", {
552
+ message_id: z.string().describe("Message ID from inbox listing"),
553
+ }, async ({ message_id }) => withBaaCheck(async () => {
554
+ const { username } = getCredentials();
555
+ const res = await authedFetch("/email/read", { address: username, message_id });
556
+ return textResult(res);
557
+ }));
558
+ server.tool("chaprola_email_send", "Send an email from your @chaprola.org address. Subject to content moderation", {
559
+ to: z.string().describe("Recipient email address"),
560
+ subject: z.string().describe("Email subject"),
561
+ text: z.string().describe("Plain text body"),
562
+ html: z.string().optional().describe("HTML body"),
563
+ from: z.string().optional().describe("Sender local part (default: your username)"),
564
+ }, async ({ to, subject, text, html, from }) => withBaaCheck(async () => {
565
+ const { username } = getCredentials();
566
+ const body = { from: from || username, to, subject, text };
567
+ if (html)
568
+ body.html = html;
569
+ const res = await authedFetch("/email/send", body);
570
+ return textResult(res);
571
+ }));
572
+ server.tool("chaprola_email_delete", "Delete a specific email from your mailbox", {
573
+ message_id: z.string().describe("Message ID to delete"),
574
+ }, async ({ message_id }) => withBaaCheck(async () => {
575
+ const { username } = getCredentials();
576
+ const res = await authedFetch("/email/delete", { address: username, message_id });
577
+ return textResult(res);
578
+ }));
579
+ // --- Start server ---
580
+ async function main() {
581
+ const transport = new StdioServerTransport();
582
+ await server.connect(transport);
583
+ }
584
+ main().catch((err) => {
585
+ console.error("MCP server error:", err);
586
+ process.exit(1);
587
+ });
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@chaprola/mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for Chaprola — agent-first data platform. Gives AI agents 35 tools for structured data storage, querying, compilation, and execution via plain HTTP.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "chaprola-mcp": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "references"
13
+ ],
14
+ "scripts": {
15
+ "start": "node dist/index.js",
16
+ "build": "tsc",
17
+ "dev": "tsx src/index.ts",
18
+ "prepublishOnly": "npm run build"
19
+ },
20
+ "keywords": [
21
+ "mcp",
22
+ "model-context-protocol",
23
+ "chaprola",
24
+ "data-platform",
25
+ "ai-agent",
26
+ "serverless",
27
+ "healthcare",
28
+ "hipaa"
29
+ ],
30
+ "author": "Charles Letcher",
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/cletcher/chaprola"
35
+ },
36
+ "homepage": "https://chaprola.org",
37
+ "engines": {
38
+ "node": ">=18"
39
+ },
40
+ "dependencies": {
41
+ "@modelcontextprotocol/sdk": "^1.12.1",
42
+ "zod": "^3.24.4"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^25.5.0",
46
+ "tsx": "^4.19.4",
47
+ "typescript": "^5.8.3"
48
+ }
49
+ }
@@ -0,0 +1,63 @@
1
+ # Chaprola Authentication — Agent Reference
2
+
3
+ ## API Key Model
4
+
5
+ Chaprola uses API key authentication. Every protected request requires:
6
+
7
+ ```
8
+ Authorization: Bearer chp_a1b2c3d4...
9
+ ```
10
+
11
+ API keys have the format `chp_` + 64 hex characters (256 bits entropy, total 68 chars).
12
+
13
+ ## Getting an API Key
14
+
15
+ ```bash
16
+ # Register (one-time)
17
+ POST /register {"username": "my-agent", "passcode": "a-long-secure-passcode-16-chars-min"}
18
+ # Response: {"status": "registered", "username": "my-agent", "api_key": "chp_..."}
19
+
20
+ # Login (generates new key, INVALIDATES previous key)
21
+ POST /login {"username": "my-agent", "passcode": "a-long-secure-passcode-16-chars-min"}
22
+ # Response: {"status": "authenticated", "username": "my-agent", "api_key": "chp_..."}
23
+ ```
24
+
25
+ ## Key Facts
26
+
27
+ - **API keys never expire.** Only invalidated by re-login or account deletion.
28
+ - **Login replaces the key.** Old key stops working immediately. Save the new one.
29
+ - **Passcode requirements:** 16-128 characters.
30
+ - **Username requirements:** 3-40 chars, alphanumeric + hyphens/underscores, starts with letter.
31
+ - **Userid must match.** Every request body's `userid` field must match the authenticated username (403 if not).
32
+ - **Rate limits:** Auth endpoints: 5 req/sec (burst 10). All others: 20 req/sec (burst 50).
33
+
34
+ ## BAA (Business Associate Agreement)
35
+
36
+ All data endpoints require a signed BAA. Without it, requests return 403.
37
+
38
+ **Flow:**
39
+ 1. `POST /baa-text` → get BAA text (show to human)
40
+ 2. Human reviews and agrees
41
+ 3. `POST /sign-baa` → sign it (one-time per account)
42
+ 4. `POST /baa-status` → verify signing status
43
+
44
+ **Exempt endpoints** (no BAA required): /hello, /register, /login, /check-username, /delete-account, /sign-baa, /baa-status, /baa-text, /report, /email/inbound
45
+
46
+ ## MCP Server Environment Variables
47
+
48
+ | Variable | Description |
49
+ |----------|-------------|
50
+ | `CHAPROLA_USERNAME` | Your registered username |
51
+ | `CHAPROLA_API_KEY` | Your API key (`chp_...`) |
52
+
53
+ These are read by the MCP server and injected into every authenticated request automatically.
54
+
55
+ ## Credential Recovery
56
+
57
+ If your API key stops working (403):
58
+ 1. Re-login with your passcode → get new API key
59
+ 2. If passcode lost → admin must create `s3://chaprola-2026/admin/reset/{username}.reset`, then login with any new passcode
60
+
61
+ ## Account Cleanup
62
+
63
+ Accounts inactive for 90 days (no authenticated API calls) are automatically deleted with all files.
@@ -0,0 +1,152 @@
1
+ # Chaprola Cookbook — Quick Reference
2
+
3
+ ## Workflow: Import → Compile → Run
4
+
5
+ ```bash
6
+ # 1. Import JSON data
7
+ POST /import {userid, project, name: "STAFF", data: [{name: "Alice", salary: 95000}, ...]}
8
+
9
+ # 2. Compile source (pass primary_format for field-name addressing)
10
+ POST /compile {userid, project, name: "REPORT", source: "...", primary_format: "STAFF"}
11
+
12
+ # 3. Run program
13
+ POST /run {userid, project, name: "REPORT", primary_file: "STAFF", record: 1}
14
+ ```
15
+
16
+ ## Hello World (no data file)
17
+
18
+ ```chaprola
19
+ MOVE "Hello from Chaprola!" U.1 20
20
+ PRINT 0
21
+ END
22
+ ```
23
+
24
+ ## Loop Through All Records
25
+
26
+ ```chaprola
27
+ DEFINE VARIABLE rec R1
28
+ LET rec = 1
29
+ 100 SEEK rec
30
+ IF EOF GOTO 900
31
+ MOVE BLANKS U.1 40
32
+ MOVE P.name U.1 8
33
+ MOVE P.salary U.12 6
34
+ PRINT 0
35
+ LET rec = rec + 1
36
+ GOTO 100
37
+ 900 END
38
+ ```
39
+
40
+ ## Filtered Report
41
+
42
+ ```chaprola
43
+ GET sal FROM P.salary
44
+ IF sal LT 80000 GOTO 200 // skip low earners
45
+ MOVE P.name U.1 8
46
+ PUT sal INTO U.12 10 D 0 // D=dollar format
47
+ PRINT 0
48
+ 200 LET rec = rec + 1
49
+ ```
50
+
51
+ ## JOIN Two Files (FIND)
52
+
53
+ ```chaprola
54
+ OPEN "DEPARTMENTS" 0
55
+ FIND match FROM S.dept_code 3 USING P.dept_code
56
+ IF match EQ 0 GOTO 200 // no match
57
+ READ match // load matched secondary record
58
+ MOVE S.dept_name U.12 15 // now accessible
59
+ ```
60
+
61
+ Compile with: `primary_format: "EMPLOYEES", secondary_formats: ["DEPARTMENTS"]`
62
+
63
+ ## Read-Modify-Write (UPDATE)
64
+
65
+ ```chaprola
66
+ READ match // load record
67
+ GET bal FROM S.balance // read current value
68
+ LET bal = bal + amt // modify
69
+ PUT bal INTO S.balance 8 F 0 // write back to S memory
70
+ WRITE match // flush to disk
71
+ CLOSE // flush all at end
72
+ ```
73
+
74
+ ## Async for Large Datasets
75
+
76
+ ```bash
77
+ # Start async job
78
+ POST /run {userid, project, name, primary_file, async: true}
79
+ # Response: {status: "running", job_id: "..."}
80
+
81
+ # Poll until done
82
+ POST /run/status {userid, project, job_id}
83
+ # Response: {status: "done", output: "..."}
84
+ ```
85
+
86
+ ## PUT Format Codes
87
+
88
+ | Code | Description | Example |
89
+ |------|-------------|---------|
90
+ | `D` | Dollar with commas | `$1,234.56` |
91
+ | `F` | Fixed decimal | `1234.56` |
92
+ | `I` | Integer (right-justified) | ` 1234` |
93
+ | `E` | Scientific notation | `1.23E+03` |
94
+
95
+ Syntax: `PUT R1 INTO U.30 10 D 2` (R-var, location, width, format, decimals)
96
+
97
+ ## Memory Regions
98
+
99
+ | Prefix | Description |
100
+ |--------|-------------|
101
+ | `P` | Primary data file (current record) |
102
+ | `S` | Secondary data file (current record) |
103
+ | `U` | User buffer (scratch for output) |
104
+ | `X` | System text (date, time, filenames) |
105
+
106
+ ## Math Intrinsics
107
+
108
+ ```chaprola
109
+ LET R2 = EXP R1 // e^R1
110
+ LET R2 = LOG R1 // ln(R1)
111
+ LET R2 = SQRT R1 // √R1
112
+ LET R2 = ABS R1 // |R1|
113
+ LET R3 = POW R1 R2 // R1^R2
114
+ ```
115
+
116
+ ## Import-Download: URL → Dataset (Parquet, Excel, CSV, JSON)
117
+
118
+ ```bash
119
+ # Import Parquet from a cloud data lake
120
+ POST /import-download {
121
+ userid, project, name: "TRIPS",
122
+ url: "https://example.com/data.parquet",
123
+ instructions: "Extract date, passenger_count, fare (2 decimals). Skip null fares.",
124
+ max_rows: 100000
125
+ }
126
+
127
+ # Import Excel spreadsheet
128
+ POST /import-download {
129
+ userid, project, name: "SALES",
130
+ url: "https://example.com/report.xlsx",
131
+ instructions: "Extract Country, Product, Units_Sold (integer), Profit (2 decimals)."
132
+ }
133
+ ```
134
+
135
+ Supports: CSV, TSV, JSON, NDJSON, Parquet (zstd/snappy/lz4), Excel (.xlsx/.xls).
136
+ AI instructions are optional — omit to import all columns as-is.
137
+ Lambda: 10 GB /tmp, 900s timeout, 500 MB download limit.
138
+
139
+ ## HULDRA Optimization Pattern
140
+
141
+ R1–R20 = elements (HULDRA sets before each run)
142
+ R21–R40 = objectives (your program computes, HULDRA reads after)
143
+ R41–R50 = scratch space
144
+
145
+ ```bash
146
+ POST /optimize {
147
+ userid, project, program: "FITMODEL", primary_file: "DATA",
148
+ elements: [{index: 1, label: "slope", start: 0.0, min: -100, max: 100, delta: 0.01}],
149
+ objectives: [{index: 1, label: "SSR", goal: 0.0, weight: 1.0}],
150
+ max_iterations: 100
151
+ }
152
+ ```
@@ -0,0 +1,87 @@
1
+ # Chaprola API — Endpoint Reference
2
+
3
+ Base URL: `https://api.chaprola.org`
4
+
5
+ Auth: `Authorization: Bearer chp_your_api_key` on all protected endpoints.
6
+
7
+ ## Public Endpoints
8
+
9
+ | Endpoint | Description |
10
+ |----------|-------------|
11
+ | `GET /hello` | Health check. Optional `?name=X` query param |
12
+ | `POST /register` | `{username, passcode}` → `{api_key}`. Passcode: 16-128 chars |
13
+ | `POST /login` | `{username, passcode}` → `{api_key}`. **Invalidates previous key** |
14
+ | `POST /check-username` | `{username}` → `{available: bool}` |
15
+ | `POST /delete-account` | `{username, passcode}` → deletes account + all data |
16
+ | `POST /baa-text` | `{}` → `{baa_version, text}`. Get BAA for human review |
17
+ | `POST /report` | `{userid, project, name}` → program output. Program must be published |
18
+
19
+ ## Protected Endpoints (auth required)
20
+
21
+ ### BAA
22
+ | Endpoint | Body | Response |
23
+ |----------|------|----------|
24
+ | `POST /sign-baa` | `{userid, signatory_name, signatory_title?, organization?}` | `{status: "signed", baa_version, signed_at}` |
25
+ | `POST /baa-status` | `{userid}` | `{signed: bool, ...details}` |
26
+
27
+ ### Data Import/Export
28
+ | Endpoint | Body | Response |
29
+ |----------|------|----------|
30
+ | `POST /import` | `{userid, project, name, data, format?, expires_in_days?}` | `{records, fields, record_length}` |
31
+ | `POST /import-url` | `{userid, project, name}` | `{upload_url, staging_key, expires_in}` |
32
+ | `POST /import-process` | `{userid, project, name, format?}` | Same as /import |
33
+ | `POST /import-download` | `{userid, project, name, url, instructions?, max_rows?}` | Same as /import |
34
+ | `POST /export` | `{userid, project, name}` | `{data: [...records]}` |
35
+ | `POST /list` | `{userid, project, pattern?}` | `{files: [...], total}` |
36
+ | `POST /download` | `{userid, project, file, type}` | `{download_url, expires_in, size_bytes}` |
37
+
38
+ ### Compile & Run
39
+ | Endpoint | Body | Response |
40
+ |----------|------|----------|
41
+ | `POST /compile` | `{userid, project, name, source, primary_format?, secondary_formats?}` | `{instructions, bytes}` |
42
+ | `POST /run` | `{userid, project, name, primary_file?, record?, async?, nophi?}` | `{output, registers}` or `{job_id}` |
43
+ | `POST /run/status` | `{userid, project, job_id}` | `{status: "running"/"done", output?}` |
44
+ | `POST /publish` | `{userid, project, name, primary_file?, record?}` | `{report_url}` |
45
+ | `POST /unpublish` | `{userid, project, name}` | `{status: "ok"}` |
46
+ | `POST /export-report` | `{userid, project, name, primary_file?, format?, title?, nophi?}` | `{output, files_written}` |
47
+
48
+ ### Query & Data Operations
49
+ | Endpoint | Body | Response |
50
+ |----------|------|----------|
51
+ | `POST /query` | `{userid, project, file, where?, select?, aggregate?, order_by?, limit?, join?, pivot?, mercury?}` | `{records, total}` |
52
+ | `POST /sort` | `{userid, project, file, sort_by}` | `{status: "ok"}` |
53
+ | `POST /index` | `{userid, project, file, field}` | `{status: "ok"}` |
54
+ | `POST /merge` | `{userid, project, file_a, file_b, output, key}` | `{status: "ok"}` |
55
+
56
+ ### Optimization (HULDRA)
57
+ | Endpoint | Body | Response |
58
+ |----------|------|----------|
59
+ | `POST /optimize` | `{userid, project, program, primary_file, elements, objectives, max_iterations?, async?}` | `{status, iterations, final_q, elements, objectives}` |
60
+ | `POST /optimize/status` | `{userid, project, job_id}` | `{status: "running"/"converged"}` |
61
+
62
+ ### Email
63
+ | Endpoint | Body | Response |
64
+ |----------|------|----------|
65
+ | `POST /email/inbox` | `{address, limit?, before?}` | `{emails: [...], total}` |
66
+ | `POST /email/read` | `{address, message_id}` | `{email: {from, to, subject, text, html}}` |
67
+ | `POST /email/send` | `{from, to, subject, text, html?}` | `{status: "sent", message_id}` |
68
+ | `POST /email/delete` | `{address, message_id}` | `{status: "deleted"}` |
69
+
70
+ ## Error Codes
71
+
72
+ | Code | Meaning |
73
+ |------|---------|
74
+ | 400 | Invalid input |
75
+ | 401 | Invalid or missing API key. Re-login with `/login` |
76
+ | 403 | BAA not signed, or userid mismatch |
77
+ | 404 | Resource not found |
78
+ | 409 | Username taken (registration) or BAA already signed |
79
+ | 429 | Rate limited. Auth: 5 rps. Others: 20 rps |
80
+ | 500 | Server error |
81
+
82
+ ## Key Rules
83
+
84
+ - `userid` in every request body must match the authenticated user (403 if not)
85
+ - API keys never expire. Login generates a new key and invalidates the old one
86
+ - Data endpoints require a signed BAA (403 if unsigned)
87
+ - All `.DA` files expire after 90 days by default
@@ -0,0 +1,84 @@
1
+ # Chaprola Gotchas — Hard-Won Lessons
2
+
3
+ ## Language
4
+
5
+ ### No parentheses in LET
6
+ Chaprola's LET supports one operation: `LET var = a OP b`. Use temp variables for complex math.
7
+ ```chaprola
8
+ // WRONG: LET result = price * (qty + bonus)
9
+ // RIGHT:
10
+ LET temp = qty + bonus
11
+ LET result = price * temp
12
+ ```
13
+
14
+ ### IF EQUAL compares a literal to a location
15
+ Cannot compare two memory locations. Copy to U buffer first.
16
+ ```chaprola
17
+ MOVE P.txn_type U.76 6
18
+ IF EQUAL "CREDIT" U.76 GOTO 200
19
+ ```
20
+
21
+ ### MOVE length must match field width
22
+ `MOVE P.name U.1 20` copies 20 chars starting at the field — if `name` is 8 chars wide, the extra 12 bleed into adjacent fields. Always match the format file width.
23
+
24
+ ### DEFINE VARIABLE names must not collide with field names
25
+ If the format has a `balance` field, don't `DEFINE VARIABLE balance R3`. Use `bal` instead. The compiler confuses the alias with the field name.
26
+
27
+ ### R-variables are floating point
28
+ All R1–R50 are 64-bit floats. `7 / 2 = 3.5`. Use PUT with `I` format to display as integer.
29
+
30
+ ### Statement numbers are labels, not line numbers
31
+ Only number lines that are GOTO/CALL targets. Don't number every line.
32
+
33
+ ### FIND returns 0 on no match
34
+ Always check `IF match EQ 0` after FIND before calling READ.
35
+
36
+ ### PRINT 0 clears the U buffer
37
+ After PRINT 0, the buffer is empty. No need to manually clear between prints unless reusing specific positions.
38
+
39
+ ## Import
40
+
41
+ ### Field widths come from the longest value
42
+ Import auto-sizes fields to the max value length. For specific widths (e.g., 8-char balance starting at 0), import as zero-padded string: `"balance": "00000000"`.
43
+
44
+ ### Use explicit OPEN record length for string-imported numbers
45
+ When numeric data is imported as strings, auto-detect (`OPEN "file" 0`) may miscalculate. Use the `record_length` from the import response: `OPEN "ACCOUNTS" 33`.
46
+
47
+ ## API
48
+
49
+ ### userid must match authenticated user
50
+ Every request body's `userid` must equal your username. 403 on mismatch.
51
+
52
+ ### Login invalidates the old key
53
+ `POST /login` generates a new API key. The old one is dead. Save the new one immediately.
54
+
55
+ ### BAA required for data operations
56
+ All import/export/compile/run/query/email endpoints return 403 without a signed BAA. Check with `/baa-status` first.
57
+
58
+ ### Async for large datasets
59
+ `POST /run` with `async: true` for >100K records. API Gateway has a 30-second timeout; async bypasses it. Poll `/run/status` until `status: "done"`.
60
+
61
+ ### secondary_formats is an array
62
+ Pass `secondary_formats: ["DEPARTMENTS"]` (array), not `secondary_format: "DEPARTMENTS"` (string), to `/compile`.
63
+
64
+ ### Data files expire
65
+ Default 90 days. Set `expires_in_days` on import to override. Expired files are deleted daily at 03:00 UTC.
66
+
67
+ ## Secondary Files
68
+
69
+ ### One at a time
70
+ Only one secondary file can be open. CLOSE before opening another. Save any needed values in R-variables or U buffer first.
71
+
72
+ ### CLOSE flushes writes
73
+ Always CLOSE before END if you wrote to the secondary file. Unflushed writes are lost.
74
+
75
+ ## Email
76
+
77
+ ### Content moderation on outbound
78
+ All outbound emails are AI-screened. Blocked emails return 403.
79
+
80
+ ### Rate limits
81
+ 20 emails/day per user, 3 emails/minute. Exceeding returns 429.
82
+
83
+ ### PHI in email
84
+ Emails containing PHI identifiers (names, SSNs, dates of birth, etc.) are blocked by the content moderator.