@chaprola/mcp-server 1.4.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +59 -0
- package/package.json +1 -1
- package/references/auth.md +2 -4
- package/references/cookbook.md +140 -1
- package/references/endpoints.md +7 -7
- package/references/gotchas.md +31 -2
package/dist/index.js
CHANGED
|
@@ -695,6 +695,14 @@ server.tool("chaprola_email_delete", "Delete a specific email from your mailbox"
|
|
|
695
695
|
const res = await authedFetch("/email/delete", { address: username, message_id });
|
|
696
696
|
return textResult(res);
|
|
697
697
|
}));
|
|
698
|
+
server.tool("chaprola_email_forward", "Forward an email (with attachments) to another address. Reads the original email from your mailbox, includes all attachments, and sends via Resend.", {
|
|
699
|
+
message_id: z.string().describe("Message ID of the email to forward"),
|
|
700
|
+
to: z.string().describe("Destination email address"),
|
|
701
|
+
}, async ({ message_id, to }) => withBaaCheck(async () => {
|
|
702
|
+
const { username } = getCredentials();
|
|
703
|
+
const res = await authedFetch("/email/forward", { from: username, message_id, to });
|
|
704
|
+
return textResult(res);
|
|
705
|
+
}));
|
|
698
706
|
// --- Search ---
|
|
699
707
|
server.tool("chaprola_search", "Search the web via Brave Search API. Returns titles, URLs, and snippets. Optional AI-grounded summary. Rate limit: 10/day per user", {
|
|
700
708
|
query: z.string().describe("Search query string"),
|
|
@@ -784,6 +792,57 @@ server.tool("chaprola_consolidate", "Merge a .MRG file into its parent .DA, prod
|
|
|
784
792
|
const res = await authedFetch("/consolidate", { userid: username, project, file });
|
|
785
793
|
return textResult(res);
|
|
786
794
|
}));
|
|
795
|
+
// --- Site Keys ---
|
|
796
|
+
server.tool("chaprola_create_site_key", "Create an origin-locked site key for frontend JavaScript. Site keys are restricted to specific origins and endpoints, safe to embed in public code.", {
|
|
797
|
+
label: z.string().describe("Human-readable label for this key (e.g., 'poll-frontend')"),
|
|
798
|
+
allowed_origins: z.array(z.string()).describe("HTTPS URL patterns where this key works (e.g., ['https://chaprola.org/apps/poll/*']). Wildcards allowed at end."),
|
|
799
|
+
allowed_endpoints: z.array(z.string()).describe("API endpoints this key can call (e.g., ['/query', '/insert-record', '/report']). Security-sensitive endpoints like /export, /import, /compile are always denied."),
|
|
800
|
+
}, async ({ label, allowed_origins, allowed_endpoints }) => {
|
|
801
|
+
const { username } = getCredentials();
|
|
802
|
+
const res = await authedFetch("/create-site-key", {
|
|
803
|
+
userid: username,
|
|
804
|
+
label,
|
|
805
|
+
allowed_origins,
|
|
806
|
+
allowed_endpoints,
|
|
807
|
+
});
|
|
808
|
+
return textResult(res);
|
|
809
|
+
});
|
|
810
|
+
server.tool("chaprola_list_site_keys", "List all site keys for the authenticated user. Shows label, allowed origins/endpoints, and creation date.", {}, async () => {
|
|
811
|
+
const { username } = getCredentials();
|
|
812
|
+
const res = await authedFetch("/list-site-keys", { userid: username });
|
|
813
|
+
return textResult(res);
|
|
814
|
+
});
|
|
815
|
+
server.tool("chaprola_delete_site_key", "Delete a site key. Requires the full site key value (site_...).", {
|
|
816
|
+
site_key: z.string().describe("The full site key to delete (starts with site_)"),
|
|
817
|
+
}, async ({ site_key }) => {
|
|
818
|
+
const { username } = getCredentials();
|
|
819
|
+
const res = await authedFetch("/delete-site-key", { userid: username, site_key });
|
|
820
|
+
return textResult(res);
|
|
821
|
+
});
|
|
822
|
+
// --- App Hosting tools ---
|
|
823
|
+
server.tool("chaprola_app_deploy", "Deploy a static web app (HTML/JS/CSS) to chaprola.org/apps/{userid}/{project}/. Returns a presigned upload URL for a .zip or .tar.gz archive.", {
|
|
824
|
+
project: z.string().describe("Project name for the app"),
|
|
825
|
+
}, async ({ project }) => withBaaCheck(async () => {
|
|
826
|
+
const { username } = getCredentials();
|
|
827
|
+
const res = await authedFetch("/app/deploy", { userid: username, project });
|
|
828
|
+
return textResult(res);
|
|
829
|
+
}));
|
|
830
|
+
server.tool("chaprola_app_deploy_process", "Process a staged app archive and deploy files to chaprola.org/apps/{userid}/{project}/", {
|
|
831
|
+
project: z.string().describe("Project name for the app"),
|
|
832
|
+
staging_key: z.string().describe("Staging key returned by chaprola_app_deploy"),
|
|
833
|
+
}, async ({ project, staging_key }) => withBaaCheck(async () => {
|
|
834
|
+
const { username } = getCredentials();
|
|
835
|
+
const res = await authedFetch("/app/deploy/process", { userid: username, project, staging_key });
|
|
836
|
+
return textResult(res);
|
|
837
|
+
}));
|
|
838
|
+
server.tool("chaprola_app_upload", "Get a presigned URL to upload a single file to a deployed app at chaprola.org/apps/{userid}/{project}/{path}", {
|
|
839
|
+
project: z.string().describe("Project name for the app"),
|
|
840
|
+
path: z.string().describe("File path within the app (e.g. 'css/app.css', 'index.html')"),
|
|
841
|
+
}, async ({ project, path }) => withBaaCheck(async () => {
|
|
842
|
+
const { username } = getCredentials();
|
|
843
|
+
const res = await authedFetch("/app/upload", { userid: username, project, path });
|
|
844
|
+
return textResult(res);
|
|
845
|
+
}));
|
|
787
846
|
// --- Start server ---
|
|
788
847
|
async function main() {
|
|
789
848
|
const transport = new StdioServerTransport();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chaprola/mcp-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "MCP server for Chaprola — agent-first data platform. Gives AI agents 46 tools for structured data storage, record CRUD, querying, schema inspection, web search, URL fetching, scheduled jobs, and execution via plain HTTP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
package/references/auth.md
CHANGED
|
@@ -33,16 +33,14 @@ POST /login {"username": "my-agent", "passcode": "a-long-secure-passcode-16-char
|
|
|
33
33
|
|
|
34
34
|
## BAA (Business Associate Agreement)
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
**Only needed for PHI (Protected Health Information).** Non-PHI data works without a BAA. If your data contains no patient names, SSNs, dates of birth, or other HIPAA identifiers, skip the BAA entirely.
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
If you do handle PHI, sign the BAA once per account:
|
|
39
39
|
1. `POST /baa-text` → get BAA text (show to human)
|
|
40
40
|
2. Human reviews and agrees
|
|
41
41
|
3. `POST /sign-baa` → sign it (one-time per account)
|
|
42
42
|
4. `POST /baa-status` → verify signing status
|
|
43
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
44
|
## MCP Server Environment Variables
|
|
47
45
|
|
|
48
46
|
| Variable | Description |
|
package/references/cookbook.md
CHANGED
|
@@ -13,6 +13,30 @@ POST /compile {userid, project, name: "REPORT", source: "...", primary_format: "
|
|
|
13
13
|
POST /run {userid, project, name: "REPORT", primary_file: "STAFF", record: 1}
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
## R-Variable Ranges
|
|
17
|
+
|
|
18
|
+
| Range | Purpose | Safe for DEFINE VARIABLE? |
|
|
19
|
+
|-------|---------|--------------------------|
|
|
20
|
+
| R1–R20 | HULDRA elements (parameters) | No — HULDRA overwrites these |
|
|
21
|
+
| R21–R40 | HULDRA objectives (error metrics) | No — HULDRA reads these |
|
|
22
|
+
| R41–R50 | Scratch space | **Yes — always use R41–R50 for DEFINE VARIABLE** |
|
|
23
|
+
|
|
24
|
+
For non-HULDRA programs, R1–R40 are technically available but using R41–R50 is a good habit.
|
|
25
|
+
|
|
26
|
+
## PRINT: Output from U Buffer
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
PRINT 0 — output the ENTIRE U buffer contents, then clear it
|
|
30
|
+
PRINT N — output exactly N characters from U buffer (no clear)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Use `PRINT N` when you've placed data at specific positions and want clean output without trailing garbage. Use `PRINT 0` for quick output of everything.
|
|
34
|
+
|
|
35
|
+
```chaprola
|
|
36
|
+
MOVE "Hello" U.1 5
|
|
37
|
+
PRINT 5 // Outputs "Hello" — exactly 5 chars, no trailing spaces
|
|
38
|
+
```
|
|
39
|
+
|
|
16
40
|
## Hello World (no data file)
|
|
17
41
|
|
|
18
42
|
```chaprola
|
|
@@ -58,7 +82,25 @@ READ match // load matched secondary record
|
|
|
58
82
|
MOVE S.dept_name U.12 15 // now accessible
|
|
59
83
|
```
|
|
60
84
|
|
|
61
|
-
Compile with
|
|
85
|
+
Compile with both formats so the compiler resolves fields from both files:
|
|
86
|
+
```bash
|
|
87
|
+
POST /compile {
|
|
88
|
+
userid, project, name: "REPORT",
|
|
89
|
+
source: "...",
|
|
90
|
+
primary_format: "EMPLOYEES",
|
|
91
|
+
secondary_format: "DEPARTMENTS"
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Comparing Two Memory Locations
|
|
96
|
+
|
|
97
|
+
IF EQUAL compares a literal to a location. To compare two memory locations, copy both to U buffer:
|
|
98
|
+
|
|
99
|
+
```chaprola
|
|
100
|
+
MOVE PARAM.poll_id U.200 12
|
|
101
|
+
MOVE P.poll_id U.180 12
|
|
102
|
+
IF EQUAL U.200 U.180 12 GOTO 200 // match — jump to handler
|
|
103
|
+
```
|
|
62
104
|
|
|
63
105
|
## Read-Modify-Write (UPDATE)
|
|
64
106
|
|
|
@@ -83,6 +125,90 @@ POST /run/status {userid, project, job_id}
|
|
|
83
125
|
# Response: {status: "done", output: "..."}
|
|
84
126
|
```
|
|
85
127
|
|
|
128
|
+
## Parameterized Reports (PARAM.name)
|
|
129
|
+
|
|
130
|
+
Programs can accept named parameters from URL query strings. Use this for dynamic reports.
|
|
131
|
+
|
|
132
|
+
```chaprola
|
|
133
|
+
// Report that accepts &deck=kanji&level=3 as URL params
|
|
134
|
+
MOVE PARAM.deck U.1 20 // string param → U buffer
|
|
135
|
+
LET lvl = PARAM.level // numeric param → R variable
|
|
136
|
+
SEEK 1
|
|
137
|
+
100 IF EOF GOTO 900
|
|
138
|
+
MOVE P.deck U.30 10
|
|
139
|
+
IF EQUAL PARAM.deck U.30 GOTO 200 // filter by deck param
|
|
140
|
+
GOTO 300
|
|
141
|
+
200 GET cardlvl FROM P.level
|
|
142
|
+
IF cardlvl NE lvl GOTO 300 // filter by level param
|
|
143
|
+
MOVE P.kanji U.1 4
|
|
144
|
+
MOVE P.reading U.6 10
|
|
145
|
+
PRINT 0
|
|
146
|
+
300 LET rec = rec + 1
|
|
147
|
+
SEEK rec
|
|
148
|
+
GOTO 100
|
|
149
|
+
900 END
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Publish with: `POST /publish {userid, project, name, primary_file, acl: "authenticated"}`
|
|
153
|
+
Call with: `GET /report?userid=X&project=Y&name=Z&deck=kanji&level=3`
|
|
154
|
+
Discover params: `POST /report/params {userid, project, name}` → returns .PF schema (field names, types, widths)
|
|
155
|
+
|
|
156
|
+
## Named Output Positions (U.name)
|
|
157
|
+
|
|
158
|
+
Instead of `U.1`, `U.12`, etc., use named positions for readable code:
|
|
159
|
+
|
|
160
|
+
```chaprola
|
|
161
|
+
// U.name positions are auto-allocated by the compiler
|
|
162
|
+
MOVE P.name U.name 20
|
|
163
|
+
MOVE P.dept U.dept 10
|
|
164
|
+
PUT sal INTO U.salary 10 D 0
|
|
165
|
+
PRINT 0
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## GROUP BY with Pivot (via /query)
|
|
169
|
+
|
|
170
|
+
Chaprola's pivot IS GROUP BY. Set `row` = grouping field, `values` = aggregate functions.
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
# SQL: SELECT department, COUNT(*), AVG(salary) FROM staff GROUP BY department
|
|
174
|
+
POST /query {
|
|
175
|
+
userid, project, file: "STAFF",
|
|
176
|
+
pivot: {
|
|
177
|
+
row: "department",
|
|
178
|
+
values: [
|
|
179
|
+
{field: "department", function: "count"},
|
|
180
|
+
{field: "salary", function: "avg"}
|
|
181
|
+
]
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
# SQL: SELECT department, year, SUM(revenue) FROM sales GROUP BY department, year
|
|
186
|
+
POST /query {
|
|
187
|
+
userid, project, file: "SALES",
|
|
188
|
+
pivot: {
|
|
189
|
+
row: "department",
|
|
190
|
+
column: "year",
|
|
191
|
+
values: [{field: "revenue", function: "sum"}],
|
|
192
|
+
totals: true
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
For simple aggregation without a cross-tabulation column, set `column` to empty string:
|
|
198
|
+
```bash
|
|
199
|
+
# Count records per department (no cross-tab)
|
|
200
|
+
POST /query {
|
|
201
|
+
userid, project, file: "STAFF",
|
|
202
|
+
pivot: {
|
|
203
|
+
row: "department",
|
|
204
|
+
column: "",
|
|
205
|
+
values: [{field: "department", function: "count"}]
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Supported aggregate functions: `count`, `sum`, `avg`, `min`, `max`, `stddev`.
|
|
211
|
+
|
|
86
212
|
## PUT Format Codes
|
|
87
213
|
|
|
88
214
|
| Code | Description | Example |
|
|
@@ -94,6 +220,19 @@ POST /run/status {userid, project, job_id}
|
|
|
94
220
|
|
|
95
221
|
Syntax: `PUT R1 INTO U.30 10 D 2` (R-var, location, width, format, decimals)
|
|
96
222
|
|
|
223
|
+
## Common Field Widths
|
|
224
|
+
|
|
225
|
+
| Data type | Chars | Example |
|
|
226
|
+
|-----------|-------|---------|
|
|
227
|
+
| ISO datetime | 20 | `2026-03-28T14:30:00Z` |
|
|
228
|
+
| UUID | 36 | `550e8400-e29b-41d4-a716-446655440000` |
|
|
229
|
+
| Email | 50 | `user@example.com` |
|
|
230
|
+
| Short ID | 8–12 | `poll_001` |
|
|
231
|
+
| Dollar amount | 10 | `$1,234.56` |
|
|
232
|
+
| Phone | 15 | `+1-555-123-4567` |
|
|
233
|
+
|
|
234
|
+
Use these when sizing MOVE lengths and U buffer positions.
|
|
235
|
+
|
|
97
236
|
## Memory Regions
|
|
98
237
|
|
|
99
238
|
| Prefix | Description |
|
package/references/endpoints.md
CHANGED
|
@@ -14,7 +14,7 @@ Auth: `Authorization: Bearer chp_your_api_key` on all protected endpoints.
|
|
|
14
14
|
| `POST /check-username` | `{username}` → `{available: bool}` |
|
|
15
15
|
| `POST /delete-account` | `{username, passcode}` → deletes account + all data |
|
|
16
16
|
| `POST /baa-text` | `{}` → `{baa_version, text}`. Get BAA for human review |
|
|
17
|
-
| `POST /report` | `{userid, project, name}` → program output. Program must be published |
|
|
17
|
+
| `POST /report` | `{userid, project, name, ¶m=value}` → program output. Accepts named params via URL query strings. Use `/report/params` to discover schema. Program must be published |
|
|
18
18
|
|
|
19
19
|
## Protected Endpoints (auth required)
|
|
20
20
|
|
|
@@ -41,16 +41,16 @@ Auth: `Authorization: Bearer chp_your_api_key` on all protected endpoints.
|
|
|
41
41
|
| `POST /compile` | `{userid, project, name, source, primary_format?, secondary_format?}` | `{instructions, bytes}` |
|
|
42
42
|
| `POST /run` | `{userid, project, name, primary_file?, record?, async?, nophi?}` | `{output, registers}` or `{job_id}` |
|
|
43
43
|
| `POST /run/status` | `{userid, project, job_id}` | `{status: "running"/"done", output?}` |
|
|
44
|
-
| `POST /publish` | `{userid, project, name, primary_file?, record?}` | `{report_url}` |
|
|
44
|
+
| `POST /publish` | `{userid, project, name, primary_file?, record?, acl?}` | `{report_url}`. ACL: `public` (default), `authenticated`, `owner`, `token` |
|
|
45
45
|
| `POST /unpublish` | `{userid, project, name}` | `{status: "ok"}` |
|
|
46
46
|
| `POST /export-report` | `{userid, project, name, primary_file?, format?, title?, nophi?}` | `{output, files_written}` |
|
|
47
47
|
|
|
48
48
|
### Query & Data Operations
|
|
49
49
|
| Endpoint | Body | Response |
|
|
50
50
|
|----------|------|----------|
|
|
51
|
-
| `POST /query` | `{userid, project, file, where
|
|
51
|
+
| `POST /query` | `{userid, project, file, where?: [{field, op, value}], select?, aggregate?, order_by?, limit?, join?, pivot?, mercury?}` | `{records, total}` |
|
|
52
52
|
| `POST /sort` | `{userid, project, file, sort_by}` | `{status: "ok"}` |
|
|
53
|
-
| `POST /index` | `{userid, project, file,
|
|
53
|
+
| `POST /index` | `{userid, project, file, key_fields: ["field1", "field2"], output: "INDEXNAME"}` | `{status: "ok"}` |
|
|
54
54
|
| `POST /merge` | `{userid, project, file_a, file_b, output, key}` | `{status: "ok"}` |
|
|
55
55
|
|
|
56
56
|
### Optimization (HULDRA)
|
|
@@ -82,6 +82,6 @@ Auth: `Authorization: Bearer chp_your_api_key` on all protected endpoints.
|
|
|
82
82
|
## Key Rules
|
|
83
83
|
|
|
84
84
|
- `userid` in every request body must match the authenticated user (403 if not)
|
|
85
|
-
- API keys
|
|
86
|
-
-
|
|
87
|
-
- All `.DA` files expire after 90 days by default
|
|
85
|
+
- API keys expire after 90 days. Login generates a new key (old keys remain valid until expiration)
|
|
86
|
+
- BAA only required for PHI data. Non-PHI data works without signing a BAA
|
|
87
|
+
- All `.DA` files expire after 90 days by default. Set `expires_in_days` on import to override (up to 36500 days)
|
package/references/gotchas.md
CHANGED
|
@@ -52,8 +52,8 @@ Every request body's `userid` must equal your username. 403 on mismatch.
|
|
|
52
52
|
### Login invalidates the old key
|
|
53
53
|
`POST /login` generates a new API key. The old one is dead. Save the new one immediately.
|
|
54
54
|
|
|
55
|
-
### BAA required for
|
|
56
|
-
|
|
55
|
+
### BAA only required for PHI
|
|
56
|
+
The BAA is only needed if your data contains Protected Health Information (patient names, SSNs, dates of birth, etc.). Non-PHI data works without signing a BAA. If you get a 403 on a PHI-flagged field, either sign the BAA or rename the field to avoid PHI auto-detection.
|
|
57
57
|
|
|
58
58
|
### Async for large datasets
|
|
59
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"`.
|
|
@@ -109,3 +109,32 @@ All outbound emails are AI-screened. Blocked emails return 403.
|
|
|
109
109
|
|
|
110
110
|
### PHI in email
|
|
111
111
|
Emails containing PHI identifiers (names, SSNs, dates of birth, etc.) are blocked by the content moderator.
|
|
112
|
+
|
|
113
|
+
## Concurrency Model
|
|
114
|
+
|
|
115
|
+
### Last-write-wins is intentional, not a missing feature
|
|
116
|
+
|
|
117
|
+
Chaprola uses **last-write-wins** instead of ACID transactions. This is a deliberate architectural choice, not a gap.
|
|
118
|
+
|
|
119
|
+
**Why last-write-wins is often the right model:**
|
|
120
|
+
|
|
121
|
+
ACID transactions solve one problem: what happens when two writers hit the same record simultaneously? Record locking, deadlock detection, lock wait queues, transaction logs, and rollback mechanisms all exist to answer that question.
|
|
122
|
+
|
|
123
|
+
For most real-world data, the answer is: they don't. Application data is typically **partitioned by owner** — a user's settings, a user's records, a user's event history. One writer per partition. No contention. In these workloads, transaction machinery is pure overhead solving a problem that doesn't exist.
|
|
124
|
+
|
|
125
|
+
Last-write-wins eliminates that overhead entirely. No lock acquisition. No lock wait. No deadlock detection. Every write goes straight through.
|
|
126
|
+
|
|
127
|
+
**What Chaprola does provide:**
|
|
128
|
+
|
|
129
|
+
- **Single-object atomicity**: Each S3 `put_object` either completes or fails — no partial writes within a single data file.
|
|
130
|
+
- **Consolidation dirty-bit checks**: The `/consolidate` endpoint verifies the merge file wasn't modified during the operation. If it was, consolidation aborts.
|
|
131
|
+
- **11-nines durability**: S3's 99.999999999% durability guarantee means your data survives once written.
|
|
132
|
+
|
|
133
|
+
**What Chaprola does not provide:**
|
|
134
|
+
|
|
135
|
+
- **Multi-object atomicity**: If you need "write record A and record B or neither," keep that logic in a relational database. Chaprola writes each object independently.
|
|
136
|
+
- **Read-modify-write isolation**: Two agents reading the same record, modifying it, and writing back will result in last-write-wins. If you need compare-and-swap semantics, Chaprola is not the right tool for that specific operation.
|
|
137
|
+
|
|
138
|
+
**When to use a relational database instead:**
|
|
139
|
+
|
|
140
|
+
When multiple concurrent writers update the same record — financial ledgers, inventory counters, auction bidding. If your application has true multi-writer contention on shared records, use PostgreSQL or similar for that workload. But most application data is owner-partitioned, and for that, last-write-wins is the lighter, faster, correct choice.
|