@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 +144 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +587 -0
- package/package.json +49 -0
- package/references/auth.md +63 -0
- package/references/cookbook.md +152 -0
- package/references/endpoints.md +87 -0
- package/references/gotchas.md +84 -0
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)
|
package/dist/index.d.ts
ADDED
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.
|