@centrali-io/centrali-mcp 4.5.0 → 4.5.2
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 +3 -3
- package/dist/tools/describe.js +50 -0
- package/dist/tools/records.js +23 -4
- package/package.json +1 -1
- package/src/tools/describe.ts +51 -0
- package/src/tools/records.ts +24 -3
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ Add to your MCP client configuration (e.g., Claude Desktop, Cursor):
|
|
|
17
17
|
"command": "npx",
|
|
18
18
|
"args": ["@centrali-io/centrali-mcp"],
|
|
19
19
|
"env": {
|
|
20
|
-
"CENTRALI_URL": "https://
|
|
20
|
+
"CENTRALI_URL": "https://centrali.io",
|
|
21
21
|
"CENTRALI_CLIENT_ID": "<service-account-client-id>",
|
|
22
22
|
"CENTRALI_CLIENT_SECRET": "<service-account-secret>",
|
|
23
23
|
"CENTRALI_WORKSPACE": "my-workspace"
|
|
@@ -31,7 +31,7 @@ Add to your MCP client configuration (e.g., Claude Desktop, Cursor):
|
|
|
31
31
|
|
|
32
32
|
| Variable | Required | Description |
|
|
33
33
|
|----------|----------|-------------|
|
|
34
|
-
| `CENTRALI_URL` | Yes | Centrali instance URL (e.g., `https://
|
|
34
|
+
| `CENTRALI_URL` | Yes | Centrali instance URL (e.g., `https://centrali.io`) |
|
|
35
35
|
| `CENTRALI_CLIENT_ID` | Yes | Service account client ID |
|
|
36
36
|
| `CENTRALI_CLIENT_SECRET` | Yes | Service account client secret |
|
|
37
37
|
| `CENTRALI_WORKSPACE` | Yes | Workspace slug to operate in |
|
|
@@ -268,7 +268,7 @@ npm install
|
|
|
268
268
|
npm run build
|
|
269
269
|
|
|
270
270
|
# Test with MCP inspector
|
|
271
|
-
CENTRALI_URL=https://
|
|
271
|
+
CENTRALI_URL=https://centrali.io \
|
|
272
272
|
CENTRALI_CLIENT_ID=... \
|
|
273
273
|
CENTRALI_CLIENT_SECRET=... \
|
|
274
274
|
CENTRALI_WORKSPACE=my-workspace \
|
package/dist/tools/describe.js
CHANGED
|
@@ -368,6 +368,37 @@ function registerDescribeTools(server) {
|
|
|
368
368
|
"}",
|
|
369
369
|
].join("\n"),
|
|
370
370
|
},
|
|
371
|
+
media_metadata: {
|
|
372
|
+
description: "When audio or video files are uploaded, Centrali automatically extracts metadata (duration, codec, resolution, bitrate) using FFprobe and stores it on the file record.",
|
|
373
|
+
api_response: "GET /files/meta/{id} returns duration (seconds), width, height, codec, and bitrate fields for media files. These fields are null for non-media files.",
|
|
374
|
+
render_url_headers: {
|
|
375
|
+
description: "The render URL response includes media metadata as HTTP headers — no separate API call needed.",
|
|
376
|
+
headers: {
|
|
377
|
+
"X-Media-Duration": "Duration in seconds (e.g., '42.350')",
|
|
378
|
+
"X-Media-Width": "Video width in pixels",
|
|
379
|
+
"X-Media-Height": "Video height in pixels",
|
|
380
|
+
"X-Media-Codec": "Primary codec (e.g., 'opus', 'aac', 'h264')",
|
|
381
|
+
"X-Media-Bitrate": "Bitrate in bits per second",
|
|
382
|
+
},
|
|
383
|
+
note: "Headers are only present when metadata is available. Use a HEAD request to retrieve metadata without downloading the file. All X-Media-* headers are exposed via CORS.",
|
|
384
|
+
},
|
|
385
|
+
example: [
|
|
386
|
+
"// Get duration from render URL headers (no extra API call)",
|
|
387
|
+
"const url = centrali.getFileRenderUrl(renderId);",
|
|
388
|
+
"const resp = await fetch(url, {",
|
|
389
|
+
" method: 'HEAD',",
|
|
390
|
+
" headers: { Authorization: `Bearer ${token}` }",
|
|
391
|
+
"});",
|
|
392
|
+
"const duration = parseFloat(resp.headers.get('X-Media-Duration') ?? '0');",
|
|
393
|
+
"const codec = resp.headers.get('X-Media-Codec');",
|
|
394
|
+
"",
|
|
395
|
+
"// Or get it from the file metadata API",
|
|
396
|
+
"const fileMeta = await centrali.getFileMeta(fileId);",
|
|
397
|
+
"console.log(fileMeta.duration); // 42.35 (seconds)",
|
|
398
|
+
"console.log(fileMeta.codec); // 'opus'",
|
|
399
|
+
].join("\n"),
|
|
400
|
+
supported_formats: "All formats FFprobe can parse — MP3, AAC, WAV, FLAC, OGG, WebM/Opus, MP4, WebM/VP9, MOV, AVI, and more.",
|
|
401
|
+
},
|
|
371
402
|
download_url: {
|
|
372
403
|
method: "client.getFileDownloadUrl(renderId)",
|
|
373
404
|
note: "Returns a URL that triggers a file download. Same auth rules as render URLs.",
|
|
@@ -378,6 +409,7 @@ function registerDescribeTools(server) {
|
|
|
378
409
|
"Store the renderId on a record field — then use getFileRenderUrl() to build the URL when displaying",
|
|
379
410
|
"Use image transformations for thumbnails instead of uploading multiple sizes",
|
|
380
411
|
"For private images in browser apps: either use a server-side proxy route or a publishable key with storage scope",
|
|
412
|
+
"For audio/video files: use X-Media-Duration header or file metadata API to get duration — don't rely on browser metadata parsing, which fails for some container formats (e.g., WebM/Opus)",
|
|
381
413
|
],
|
|
382
414
|
},
|
|
383
415
|
realtime: {
|
|
@@ -598,6 +630,24 @@ function registerDescribeTools(server) {
|
|
|
598
630
|
description: "Comma-separated list of reference field names to expand (join). Returns the full referenced record instead of just the ID.",
|
|
599
631
|
example: "expand: 'customer,product'",
|
|
600
632
|
},
|
|
633
|
+
dateWindow: {
|
|
634
|
+
description: "Date range filter. Restricts results to records where a date field falls within a given range. Both 'from' and 'to' are optional (open-ended ranges allowed).",
|
|
635
|
+
shape: {
|
|
636
|
+
field: "string — date field to filter on (e.g., 'createdAt', 'updatedAt', or a custom date field)",
|
|
637
|
+
from: "string? — ISO 8601 lower bound (inclusive)",
|
|
638
|
+
to: "string? — ISO 8601 upper bound (inclusive)",
|
|
639
|
+
},
|
|
640
|
+
examples: {
|
|
641
|
+
"Last 30 days": { field: "createdAt", from: "2024-03-01T00:00:00Z" },
|
|
642
|
+
"Specific range": { field: "updatedAt", from: "2024-01-01T00:00:00Z", to: "2024-03-31T23:59:59Z" },
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
includeDeleted: {
|
|
646
|
+
description: "Set to true to include soft-deleted records in the results (default: false). Alias: includeArchived, all.",
|
|
647
|
+
},
|
|
648
|
+
includeTotal: {
|
|
649
|
+
description: "Set to true to include the total record count in response metadata (default: false).",
|
|
650
|
+
},
|
|
601
651
|
upsert: {
|
|
602
652
|
description: "Atomic create-or-update. Provide 'match' fields to find existing record and 'data' for the full record body.",
|
|
603
653
|
example: {
|
package/dist/tools/records.js
CHANGED
|
@@ -90,14 +90,14 @@ function createRecordsClient(sdk, centraliUrl, workspaceId, recordSlug) {
|
|
|
90
90
|
return client;
|
|
91
91
|
}
|
|
92
92
|
function registerRecordTools(server, sdk, centraliUrl, workspaceId) {
|
|
93
|
-
server.tool("query_records", "Query records from a collection with optional filters, sorting, and
|
|
93
|
+
server.tool("query_records", "Query records from a collection with optional filters, sorting, pagination, and date range filtering. Filters use 'data.' prefix for custom fields and bracket notation for operators (e.g., 'data.status': 'active', 'data.price[lte]': 100). Use dateWindow for date range queries.", {
|
|
94
94
|
recordSlug: zod_1.z
|
|
95
95
|
.string()
|
|
96
96
|
.describe("The collection's record slug (e.g., 'orders')"),
|
|
97
97
|
filters: zod_1.z
|
|
98
98
|
.record(zod_1.z.string(), zod_1.z.any())
|
|
99
99
|
.optional()
|
|
100
|
-
.describe("Filter object with keys like 'data.fieldName' or 'data.fieldName[operator]'. Operators: eq, ne, gt, gte, lt, lte, in, nin, contains, startswith, endswith"),
|
|
100
|
+
.describe("Filter object with keys like 'data.fieldName' or 'data.fieldName[operator]'. Operators: eq, ne, gt, gte, lt, lte, in, nin, contains, startswith, endswith, hasAny, hasAll"),
|
|
101
101
|
sort: zod_1.z
|
|
102
102
|
.string()
|
|
103
103
|
.optional()
|
|
@@ -111,12 +111,31 @@ function registerRecordTools(server, sdk, centraliUrl, workspaceId) {
|
|
|
111
111
|
.string()
|
|
112
112
|
.optional()
|
|
113
113
|
.describe("Comma-separated reference fields to expand (e.g., 'customer,product')"),
|
|
114
|
-
|
|
114
|
+
dateWindow: zod_1.z
|
|
115
|
+
.object({
|
|
116
|
+
field: zod_1.z.string().describe("Date field to filter on (e.g., 'createdAt', 'updatedAt')"),
|
|
117
|
+
from: zod_1.z.string().optional().describe("ISO 8601 lower bound (inclusive)"),
|
|
118
|
+
to: zod_1.z.string().optional().describe("ISO 8601 upper bound (inclusive)"),
|
|
119
|
+
})
|
|
120
|
+
.optional()
|
|
121
|
+
.describe("Date range filter. Restricts results to records where the specified date field falls within the given range."),
|
|
122
|
+
includeDeleted: zod_1.z
|
|
123
|
+
.boolean()
|
|
124
|
+
.optional()
|
|
125
|
+
.describe("Include soft-deleted records (default: false)"),
|
|
126
|
+
includeTotal: zod_1.z
|
|
127
|
+
.boolean()
|
|
128
|
+
.optional()
|
|
129
|
+
.describe("Include total record count in response metadata (default: false)"),
|
|
130
|
+
}, (_a) => __awaiter(this, [_a], void 0, function* ({ recordSlug, filters, sort, page, pageSize, expand, dateWindow, includeDeleted, includeTotal }) {
|
|
115
131
|
try {
|
|
116
132
|
const params = Object.assign(Object.assign({}, filters), { sort,
|
|
117
133
|
page,
|
|
118
134
|
pageSize,
|
|
119
|
-
expand
|
|
135
|
+
expand,
|
|
136
|
+
dateWindow,
|
|
137
|
+
includeDeleted,
|
|
138
|
+
includeTotal });
|
|
120
139
|
// Remove undefined values
|
|
121
140
|
Object.keys(params).forEach((key) => params[key] === undefined && delete params[key]);
|
|
122
141
|
const result = yield sdk.queryRecords(recordSlug, params);
|
package/package.json
CHANGED
package/src/tools/describe.ts
CHANGED
|
@@ -376,6 +376,37 @@ export function registerDescribeTools(server: McpServer) {
|
|
|
376
376
|
"}",
|
|
377
377
|
].join("\n"),
|
|
378
378
|
},
|
|
379
|
+
media_metadata: {
|
|
380
|
+
description: "When audio or video files are uploaded, Centrali automatically extracts metadata (duration, codec, resolution, bitrate) using FFprobe and stores it on the file record.",
|
|
381
|
+
api_response: "GET /files/meta/{id} returns duration (seconds), width, height, codec, and bitrate fields for media files. These fields are null for non-media files.",
|
|
382
|
+
render_url_headers: {
|
|
383
|
+
description: "The render URL response includes media metadata as HTTP headers — no separate API call needed.",
|
|
384
|
+
headers: {
|
|
385
|
+
"X-Media-Duration": "Duration in seconds (e.g., '42.350')",
|
|
386
|
+
"X-Media-Width": "Video width in pixels",
|
|
387
|
+
"X-Media-Height": "Video height in pixels",
|
|
388
|
+
"X-Media-Codec": "Primary codec (e.g., 'opus', 'aac', 'h264')",
|
|
389
|
+
"X-Media-Bitrate": "Bitrate in bits per second",
|
|
390
|
+
},
|
|
391
|
+
note: "Headers are only present when metadata is available. Use a HEAD request to retrieve metadata without downloading the file. All X-Media-* headers are exposed via CORS.",
|
|
392
|
+
},
|
|
393
|
+
example: [
|
|
394
|
+
"// Get duration from render URL headers (no extra API call)",
|
|
395
|
+
"const url = centrali.getFileRenderUrl(renderId);",
|
|
396
|
+
"const resp = await fetch(url, {",
|
|
397
|
+
" method: 'HEAD',",
|
|
398
|
+
" headers: { Authorization: `Bearer ${token}` }",
|
|
399
|
+
"});",
|
|
400
|
+
"const duration = parseFloat(resp.headers.get('X-Media-Duration') ?? '0');",
|
|
401
|
+
"const codec = resp.headers.get('X-Media-Codec');",
|
|
402
|
+
"",
|
|
403
|
+
"// Or get it from the file metadata API",
|
|
404
|
+
"const fileMeta = await centrali.getFileMeta(fileId);",
|
|
405
|
+
"console.log(fileMeta.duration); // 42.35 (seconds)",
|
|
406
|
+
"console.log(fileMeta.codec); // 'opus'",
|
|
407
|
+
].join("\n"),
|
|
408
|
+
supported_formats: "All formats FFprobe can parse — MP3, AAC, WAV, FLAC, OGG, WebM/Opus, MP4, WebM/VP9, MOV, AVI, and more.",
|
|
409
|
+
},
|
|
379
410
|
download_url: {
|
|
380
411
|
method: "client.getFileDownloadUrl(renderId)",
|
|
381
412
|
note: "Returns a URL that triggers a file download. Same auth rules as render URLs.",
|
|
@@ -386,6 +417,7 @@ export function registerDescribeTools(server: McpServer) {
|
|
|
386
417
|
"Store the renderId on a record field — then use getFileRenderUrl() to build the URL when displaying",
|
|
387
418
|
"Use image transformations for thumbnails instead of uploading multiple sizes",
|
|
388
419
|
"For private images in browser apps: either use a server-side proxy route or a publishable key with storage scope",
|
|
420
|
+
"For audio/video files: use X-Media-Duration header or file metadata API to get duration — don't rely on browser metadata parsing, which fails for some container formats (e.g., WebM/Opus)",
|
|
389
421
|
],
|
|
390
422
|
},
|
|
391
423
|
realtime: {
|
|
@@ -642,6 +674,25 @@ export function registerDescribeTools(server: McpServer) {
|
|
|
642
674
|
"Comma-separated list of reference field names to expand (join). Returns the full referenced record instead of just the ID.",
|
|
643
675
|
example: "expand: 'customer,product'",
|
|
644
676
|
},
|
|
677
|
+
dateWindow: {
|
|
678
|
+
description:
|
|
679
|
+
"Date range filter. Restricts results to records where a date field falls within a given range. Both 'from' and 'to' are optional (open-ended ranges allowed).",
|
|
680
|
+
shape: {
|
|
681
|
+
field: "string — date field to filter on (e.g., 'createdAt', 'updatedAt', or a custom date field)",
|
|
682
|
+
from: "string? — ISO 8601 lower bound (inclusive)",
|
|
683
|
+
to: "string? — ISO 8601 upper bound (inclusive)",
|
|
684
|
+
},
|
|
685
|
+
examples: {
|
|
686
|
+
"Last 30 days": { field: "createdAt", from: "2024-03-01T00:00:00Z" },
|
|
687
|
+
"Specific range": { field: "updatedAt", from: "2024-01-01T00:00:00Z", to: "2024-03-31T23:59:59Z" },
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
includeDeleted: {
|
|
691
|
+
description: "Set to true to include soft-deleted records in the results (default: false). Alias: includeArchived, all.",
|
|
692
|
+
},
|
|
693
|
+
includeTotal: {
|
|
694
|
+
description: "Set to true to include the total record count in response metadata (default: false).",
|
|
695
|
+
},
|
|
645
696
|
upsert: {
|
|
646
697
|
description:
|
|
647
698
|
"Atomic create-or-update. Provide 'match' fields to find existing record and 'data' for the full record body.",
|
package/src/tools/records.ts
CHANGED
|
@@ -84,7 +84,7 @@ function createRecordsClient(sdk: CentraliSDK, centraliUrl: string, workspaceId:
|
|
|
84
84
|
export function registerRecordTools(server: McpServer, sdk: CentraliSDK, centraliUrl: string, workspaceId: string) {
|
|
85
85
|
server.tool(
|
|
86
86
|
"query_records",
|
|
87
|
-
"Query records from a collection with optional filters, sorting, and
|
|
87
|
+
"Query records from a collection with optional filters, sorting, pagination, and date range filtering. Filters use 'data.' prefix for custom fields and bracket notation for operators (e.g., 'data.status': 'active', 'data.price[lte]': 100). Use dateWindow for date range queries.",
|
|
88
88
|
{
|
|
89
89
|
recordSlug: z
|
|
90
90
|
.string()
|
|
@@ -93,7 +93,7 @@ export function registerRecordTools(server: McpServer, sdk: CentraliSDK, central
|
|
|
93
93
|
.record(z.string(), z.any())
|
|
94
94
|
.optional()
|
|
95
95
|
.describe(
|
|
96
|
-
"Filter object with keys like 'data.fieldName' or 'data.fieldName[operator]'. Operators: eq, ne, gt, gte, lt, lte, in, nin, contains, startswith, endswith"
|
|
96
|
+
"Filter object with keys like 'data.fieldName' or 'data.fieldName[operator]'. Operators: eq, ne, gt, gte, lt, lte, in, nin, contains, startswith, endswith, hasAny, hasAll"
|
|
97
97
|
),
|
|
98
98
|
sort: z
|
|
99
99
|
.string()
|
|
@@ -112,8 +112,26 @@ export function registerRecordTools(server: McpServer, sdk: CentraliSDK, central
|
|
|
112
112
|
.describe(
|
|
113
113
|
"Comma-separated reference fields to expand (e.g., 'customer,product')"
|
|
114
114
|
),
|
|
115
|
+
dateWindow: z
|
|
116
|
+
.object({
|
|
117
|
+
field: z.string().describe("Date field to filter on (e.g., 'createdAt', 'updatedAt')"),
|
|
118
|
+
from: z.string().optional().describe("ISO 8601 lower bound (inclusive)"),
|
|
119
|
+
to: z.string().optional().describe("ISO 8601 upper bound (inclusive)"),
|
|
120
|
+
})
|
|
121
|
+
.optional()
|
|
122
|
+
.describe(
|
|
123
|
+
"Date range filter. Restricts results to records where the specified date field falls within the given range."
|
|
124
|
+
),
|
|
125
|
+
includeDeleted: z
|
|
126
|
+
.boolean()
|
|
127
|
+
.optional()
|
|
128
|
+
.describe("Include soft-deleted records (default: false)"),
|
|
129
|
+
includeTotal: z
|
|
130
|
+
.boolean()
|
|
131
|
+
.optional()
|
|
132
|
+
.describe("Include total record count in response metadata (default: false)"),
|
|
115
133
|
},
|
|
116
|
-
async ({ recordSlug, filters, sort, page, pageSize, expand }) => {
|
|
134
|
+
async ({ recordSlug, filters, sort, page, pageSize, expand, dateWindow, includeDeleted, includeTotal }) => {
|
|
117
135
|
try {
|
|
118
136
|
const params: Record<string, any> = {
|
|
119
137
|
...filters,
|
|
@@ -121,6 +139,9 @@ export function registerRecordTools(server: McpServer, sdk: CentraliSDK, central
|
|
|
121
139
|
page,
|
|
122
140
|
pageSize,
|
|
123
141
|
expand,
|
|
142
|
+
dateWindow,
|
|
143
|
+
includeDeleted,
|
|
144
|
+
includeTotal,
|
|
124
145
|
};
|
|
125
146
|
// Remove undefined values
|
|
126
147
|
Object.keys(params).forEach(
|