@chaprola/mcp-server 1.9.0 → 1.11.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.
Files changed (2) hide show
  1. package/dist/index.js +42 -9
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -582,23 +582,26 @@ server.tool("chaprola_download", "Get a presigned S3 URL to download any file yo
582
582
  return textResult(res);
583
583
  }));
584
584
  // --- Query ---
585
- server.tool("chaprola_query", "SQL-free data query with WHERE, SELECT, aggregation, ORDER BY, JOIN, pivot, and Mercury scoring", {
585
+ server.tool("chaprola_query", "SQL-free data query with WHERE, SELECT, aggregation, ORDER BY, JOIN, pivot, DISTINCT, transforms, and Mercury scoring", {
586
586
  project: z.string().describe("Project name"),
587
587
  file: z.string().describe("Data file to query"),
588
- where: z.string().optional().describe("JSON object of filter conditions, e.g. {\"field\": \"status\", \"op\": \"eq\", \"value\": \"active\"}. Ops: eq, ne, gt, ge, lt, le, between, contains, starts_with"),
588
+ where: z.string().optional().describe("JSON array of filter conditions, e.g. [{\"field\": \"status\", \"op\": \"eq\", \"value\": \"active\"}]. Ops: eq, ne, gt, ge, lt, le, between, contains, starts_with, in, not_in, date_within, date_before, date_after. For in/not_in, value is a JSON array. For date_within, value is relative like \"30d\", \"6h\", \"15m\"."),
589
589
  select: z.array(z.string()).optional().describe("Fields to include in output"),
590
590
  aggregate: z.string().optional().describe("JSON array of aggregation specs, e.g. [{\"field\": \"amount\", \"func\": \"sum\"}]. Funcs: count, sum, avg, min, max, stddev"),
591
- order_by: z.string().optional().describe("JSON array of sort specs, e.g. [{\"field\": \"name\", \"dir\": \"asc\"}]"),
591
+ order_by: z.string().optional().describe("JSON sort spec — single object or array for multi-column sort, e.g. [{\"field\": \"dept\", \"dir\": \"asc\"}, {\"field\": \"salary\", \"dir\": \"desc\"}]"),
592
+ distinct: z.boolean().optional().describe("Return only unique rows based on selected fields. Cannot combine with aggregate or pivot."),
593
+ transform: z.string().optional().describe("JSON array of transforms applied before WHERE, e.g. [{\"field\": \"email\", \"func\": \"lower\"}]. Funcs: upper, lower, trim, substring (start, length), replace (old, new), length, date_trunc (unit: year/month/day/hour), date_extract (part: year/month/day/hour/minute/weekday)"),
592
594
  limit: z.number().optional().describe("Max results to return"),
593
595
  offset: z.number().optional().describe("Skip this many results"),
594
596
  join: z.string().optional().describe("JSON object of join config, e.g. {\"file\": \"other\", \"on\": \"id\", \"type\": \"inner\"}"),
595
597
  pivot: z.string().optional().describe("JSON object of pivot config, e.g. {\"row\": \"category\", \"column\": \"month\", \"values\": \"sales\"}"),
596
598
  mercury: z.string().optional().describe("JSON object of mercury scoring config, e.g. {\"fields\": [{\"field\": \"score\", \"target\": 100, \"weight\": 1.0}]}"),
597
599
  userid: z.string().optional().describe("Project owner's username. Required when accessing a shared project where you are a writer. Defaults to the authenticated user."),
598
- }, async ({ project, file, where: whereStr, select, aggregate: aggregateStr, order_by: orderByStr, limit, offset, join: joinStr, pivot: pivotStr, mercury: mercuryStr, userid }) => withBaaCheck(async () => {
600
+ }, async ({ project, file, where: whereStr, select, aggregate: aggregateStr, order_by: orderByStr, distinct, transform: transformStr, limit, offset, join: joinStr, pivot: pivotStr, mercury: mercuryStr, userid }) => withBaaCheck(async () => {
599
601
  const where = typeof whereStr === 'string' ? JSON.parse(whereStr) : whereStr;
600
602
  const aggregate = typeof aggregateStr === 'string' ? JSON.parse(aggregateStr) : aggregateStr;
601
603
  const order_by = typeof orderByStr === 'string' ? JSON.parse(orderByStr) : orderByStr;
604
+ const transform = transformStr ? (typeof transformStr === 'string' ? JSON.parse(transformStr) : transformStr) : undefined;
602
605
  const join = typeof joinStr === 'string' ? JSON.parse(joinStr) : joinStr;
603
606
  const pivot = typeof pivotStr === 'string' ? JSON.parse(pivotStr) : pivotStr;
604
607
  const mercury = typeof mercuryStr === 'string' ? JSON.parse(mercuryStr) : mercuryStr;
@@ -612,6 +615,10 @@ server.tool("chaprola_query", "SQL-free data query with WHERE, SELECT, aggregati
612
615
  body.aggregate = aggregate;
613
616
  if (order_by)
614
617
  body.order_by = order_by;
618
+ if (distinct)
619
+ body.distinct = distinct;
620
+ if (transform)
621
+ body.transform = transform;
615
622
  if (limit !== undefined)
616
623
  body.limit = limit;
617
624
  if (offset !== undefined)
@@ -868,15 +875,22 @@ server.tool("chaprola_schedule_delete", "Delete a scheduled job by name", {
868
875
  return textResult(res);
869
876
  }));
870
877
  // --- Record CRUD ---
871
- server.tool("chaprola_insert_record", "Insert a new record into a data file's merge file (.MRG). The record appears at the end of the file until consolidation.", {
878
+ server.tool("chaprola_insert_record", "Insert one or more records into a data file. For single insert, use record. For batch insert (up to 1000), use records. All records are validated before any writes invalid batch = no partial writes.", {
872
879
  project: z.string().describe("Project name"),
873
880
  file: z.string().describe("Data file name (without extension)"),
874
- record: z.string().describe("JSON object of the record to insert, e.g. {\"name\": \"foo\", \"status\": \"active\"}. Unspecified fields default to blanks."),
881
+ record: z.string().optional().describe("JSON object of a single record to insert, e.g. {\"name\": \"foo\", \"status\": \"active\"}. Use this OR records, not both."),
882
+ records: z.string().optional().describe("JSON array of records for batch insert (max 1000), e.g. [{\"name\": \"foo\"}, {\"name\": \"bar\"}]. Use this OR record, not both."),
875
883
  userid: z.string().optional().describe("Project owner's username. Required when accessing a shared project where you are a writer. Defaults to the authenticated user."),
876
- }, async ({ project, file, record: recordStr, userid }) => withBaaCheck(async () => {
877
- const record = typeof recordStr === 'string' ? JSON.parse(recordStr) : recordStr;
884
+ }, async ({ project, file, record: recordStr, records: recordsStr, userid }) => withBaaCheck(async () => {
885
+ const record = recordStr ? (typeof recordStr === 'string' ? JSON.parse(recordStr) : recordStr) : undefined;
886
+ const records = recordsStr ? (typeof recordsStr === 'string' ? JSON.parse(recordsStr) : recordsStr) : undefined;
878
887
  const { username } = getCredentials();
879
- const res = await authedFetch("/insert-record", { userid: userid || username, project, file, record });
888
+ const body = { userid: userid || username, project, file };
889
+ if (record)
890
+ body.record = record;
891
+ if (records)
892
+ body.records = records;
893
+ const res = await authedFetch("/insert-record", body);
880
894
  return textResult(res);
881
895
  }));
882
896
  server.tool("chaprola_update_record", "Update fields in a single record matched by a where clause. If no sort-key changes, updates in place; otherwise marks old record ignored and appends to merge file.", {
@@ -903,6 +917,25 @@ server.tool("chaprola_delete_record", "Delete a single record matched by a where
903
917
  const res = await authedFetch("/delete-record", { userid: userid || username, project, file, where: whereClause });
904
918
  return textResult(res);
905
919
  }));
920
+ server.tool("chaprola_upsert_record", "Insert or update a record by key field. If a record with the matching key value exists, update it in place. If not, insert it. Supports batch: pass records array to upsert multiple records in one call.", {
921
+ project: z.string().describe("Project name"),
922
+ file: z.string().describe("Data file name (without extension)"),
923
+ key: z.string().describe("Field name to match on, e.g. \"contact_id\". Must exist in the format file."),
924
+ record: z.string().optional().describe("JSON object of a single record to upsert, e.g. {\"contact_id\": \"c-42\", \"name\": \"Alice\"}. Must contain the key field. Use this OR records, not both."),
925
+ records: z.string().optional().describe("JSON array of records for batch upsert (max 1000). Each must contain the key field. Use this OR record, not both."),
926
+ userid: z.string().optional().describe("Project owner's username. Required when accessing a shared project where you are a writer. Defaults to the authenticated user."),
927
+ }, async ({ project, file, key, record: recordStr, records: recordsStr, userid }) => withBaaCheck(async () => {
928
+ const record = recordStr ? (typeof recordStr === 'string' ? JSON.parse(recordStr) : recordStr) : undefined;
929
+ const records = recordsStr ? (typeof recordsStr === 'string' ? JSON.parse(recordsStr) : recordsStr) : undefined;
930
+ const { username } = getCredentials();
931
+ const body = { userid: userid || username, project, file, key };
932
+ if (record)
933
+ body.record = record;
934
+ if (records)
935
+ body.records = records;
936
+ const res = await authedFetch("/upsert-record", body);
937
+ return textResult(res);
938
+ }));
906
939
  server.tool("chaprola_consolidate", "Merge a .MRG file into its parent .DA, producing a clean sorted data file. Deletes .MRG and .IGN after success. Aborts if .MRG was modified during the operation.", {
907
940
  project: z.string().describe("Project name"),
908
941
  file: z.string().describe("Data file name (without extension)"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chaprola/mcp-server",
3
- "version": "1.9.0",
3
+ "version": "1.11.0",
4
4
  "description": "MCP server for Chaprola — agent-first data platform. Gives AI agents tools for structured data storage, record CRUD, querying, schema inspection, documentation lookup, web search, URL fetching, scheduled jobs, scoped site keys, and execution via plain HTTP.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",