@centrali-io/centrali-mcp 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.
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { CentraliSDK } from "@centrali-io/centrali-sdk";
3
+ export declare function registerStructureTools(server: McpServer, sdk: CentraliSDK): void;
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.registerStructureTools = registerStructureTools;
13
+ const zod_1 = require("zod");
14
+ function registerStructureTools(server, sdk) {
15
+ server.tool("list_structures", "List all data structures (schemas) in the Centrali workspace. Returns name, slug, description, and property definitions for each structure.", {
16
+ page: zod_1.z.number().optional().describe("Page number (1-indexed)"),
17
+ limit: zod_1.z.number().optional().describe("Results per page"),
18
+ }, (_a) => __awaiter(this, [_a], void 0, function* ({ page, limit }) {
19
+ try {
20
+ const result = yield sdk.structures.list({ page, limit });
21
+ return {
22
+ content: [
23
+ { type: "text", text: JSON.stringify(result.data, null, 2) },
24
+ ],
25
+ };
26
+ }
27
+ catch (error) {
28
+ return {
29
+ content: [
30
+ {
31
+ type: "text",
32
+ text: `Error listing structures: ${error.message}`,
33
+ },
34
+ ],
35
+ isError: true,
36
+ };
37
+ }
38
+ }));
39
+ server.tool("get_structure", "Get the full schema definition for a specific structure by its record slug. Returns properties, types, constraints, and configuration.", {
40
+ recordSlug: zod_1.z
41
+ .string()
42
+ .describe("The structure's record slug (e.g., 'orders', 'customers')"),
43
+ }, (_a) => __awaiter(this, [_a], void 0, function* ({ recordSlug }) {
44
+ try {
45
+ const result = yield sdk.structures.getBySlug(recordSlug);
46
+ return {
47
+ content: [
48
+ { type: "text", text: JSON.stringify(result.data, null, 2) },
49
+ ],
50
+ };
51
+ }
52
+ catch (error) {
53
+ return {
54
+ content: [
55
+ {
56
+ type: "text",
57
+ text: `Error getting structure '${recordSlug}': ${error.message}`,
58
+ },
59
+ ],
60
+ isError: true,
61
+ };
62
+ }
63
+ }));
64
+ }
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@centrali-io/centrali-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Centrali MCP Server - AI assistant integration for Centrali workspaces",
5
+ "main": "dist/index.js",
6
+ "type": "commonjs",
7
+ "bin": {
8
+ "centrali-mcp": "./dist/index.js"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/blueinit/centrali-mcp.git"
13
+ },
14
+ "scripts": {
15
+ "build": "tsc --project tsconfig.json",
16
+ "prepublishOnly": "npm run build",
17
+ "test": "echo \"Error: no test specified\" && exit 1"
18
+ },
19
+ "keywords": ["mcp", "centrali", "ai", "model-context-protocol"],
20
+ "author": "Blueinit",
21
+ "license": "ISC",
22
+ "dependencies": {
23
+ "@centrali-io/centrali-sdk": "^3.1.0",
24
+ "@modelcontextprotocol/sdk": "^1.12.1"
25
+ },
26
+ "devDependencies": {
27
+ "typescript": "^5.8.3"
28
+ },
29
+ "publishConfig": {
30
+ "registry": "https://registry.npmjs.org/",
31
+ "access": "public"
32
+ }
33
+ }
package/src/index.ts ADDED
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { CentraliSDK } from "@centrali-io/centrali-sdk";
6
+ import { registerStructureTools } from "./tools/structures.js";
7
+ import { registerRecordTools } from "./tools/records.js";
8
+ import { registerSearchTools } from "./tools/search.js";
9
+ import { registerComputeTools } from "./tools/compute.js";
10
+ import { registerSmartQueryTools } from "./tools/smart-queries.js";
11
+ import { registerStructureResources } from "./resources/structures.js";
12
+
13
+ function getRequiredEnv(name: string): string {
14
+ const value = process.env[name];
15
+ if (!value) {
16
+ console.error(`Missing required environment variable: ${name}`);
17
+ process.exit(1);
18
+ }
19
+ return value;
20
+ }
21
+
22
+ async function main() {
23
+ const baseUrl = getRequiredEnv("CENTRALI_URL");
24
+ const clientId = getRequiredEnv("CENTRALI_CLIENT_ID");
25
+ const clientSecret = getRequiredEnv("CENTRALI_CLIENT_SECRET");
26
+ const workspaceId = getRequiredEnv("CENTRALI_WORKSPACE");
27
+
28
+ const sdk = new CentraliSDK({
29
+ baseUrl,
30
+ workspaceId,
31
+ clientId,
32
+ clientSecret,
33
+ });
34
+
35
+ const server = new McpServer({
36
+ name: "centrali",
37
+ version: "1.0.0",
38
+ });
39
+
40
+ // Register all tools
41
+ registerStructureTools(server, sdk);
42
+ registerRecordTools(server, sdk);
43
+ registerSearchTools(server, sdk);
44
+ registerComputeTools(server, sdk);
45
+ registerSmartQueryTools(server, sdk);
46
+
47
+ // Register resources
48
+ registerStructureResources(server, sdk);
49
+
50
+ // Start stdio transport
51
+ const transport = new StdioServerTransport();
52
+ await server.connect(transport);
53
+ }
54
+
55
+ main().catch((error) => {
56
+ console.error("Fatal error:", error);
57
+ process.exit(1);
58
+ });
@@ -0,0 +1,71 @@
1
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { CentraliSDK } from "@centrali-io/centrali-sdk";
3
+
4
+ export function registerStructureResources(
5
+ server: McpServer,
6
+ sdk: CentraliSDK
7
+ ) {
8
+ // Static resource: list all structures (gives LLM workspace context)
9
+ server.resource(
10
+ "structures-list",
11
+ "centrali://structures",
12
+ {
13
+ description:
14
+ "List of all data structures in the workspace with name, slug, and description",
15
+ mimeType: "application/json",
16
+ },
17
+ async (uri) => {
18
+ const result = await sdk.structures.list({ limit: 100 });
19
+ const summary = (result.data || []).map((s) => ({
20
+ name: s.name,
21
+ recordSlug: s.recordSlug,
22
+ description: s.description || "",
23
+ propertyCount: s.properties?.length || 0,
24
+ status: s.status,
25
+ }));
26
+ return {
27
+ contents: [
28
+ {
29
+ uri: uri.href,
30
+ mimeType: "application/json",
31
+ text: JSON.stringify(summary, null, 2),
32
+ },
33
+ ],
34
+ };
35
+ }
36
+ );
37
+
38
+ // Resource template: full schema for a specific structure
39
+ server.resource(
40
+ "structure-schema",
41
+ new ResourceTemplate("centrali://structures/{slug}", {
42
+ list: async () => {
43
+ const result = await sdk.structures.list({ limit: 100 });
44
+ return {
45
+ resources: (result.data || []).map((s) => ({
46
+ uri: `centrali://structures/${s.recordSlug}`,
47
+ name: s.name,
48
+ description: s.description || `Schema for ${s.name}`,
49
+ mimeType: "application/json",
50
+ })),
51
+ };
52
+ },
53
+ }),
54
+ {
55
+ description: "Full schema definition for a specific structure",
56
+ mimeType: "application/json",
57
+ },
58
+ async (uri, { slug }) => {
59
+ const result = await sdk.structures.getBySlug(slug as string);
60
+ return {
61
+ contents: [
62
+ {
63
+ uri: uri.href,
64
+ mimeType: "application/json",
65
+ text: JSON.stringify(result.data, null, 2),
66
+ },
67
+ ],
68
+ };
69
+ }
70
+ );
71
+ }
@@ -0,0 +1,120 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { CentraliSDK } from "@centrali-io/centrali-sdk";
3
+ import { z } from "zod";
4
+
5
+ export function registerComputeTools(server: McpServer, sdk: CentraliSDK) {
6
+ server.tool(
7
+ "list_functions",
8
+ "List all compute functions in the workspace. Compute functions are JavaScript code blocks that run server-side.",
9
+ {
10
+ page: z.number().optional().describe("Page number"),
11
+ limit: z.number().optional().describe("Results per page"),
12
+ },
13
+ async ({ page, limit }) => {
14
+ try {
15
+ const options: Record<string, any> = {};
16
+ if (page !== undefined) options.page = page;
17
+ if (limit !== undefined) options.limit = limit;
18
+
19
+ const result = await sdk.functions.list(
20
+ Object.keys(options).length > 0 ? options : undefined
21
+ );
22
+ return {
23
+ content: [
24
+ { type: "text", text: JSON.stringify(result.data, null, 2) },
25
+ ],
26
+ };
27
+ } catch (error: any) {
28
+ return {
29
+ content: [
30
+ {
31
+ type: "text",
32
+ text: `Error listing functions: ${error.message}`,
33
+ },
34
+ ],
35
+ isError: true,
36
+ };
37
+ }
38
+ }
39
+ );
40
+
41
+ server.tool(
42
+ "list_triggers",
43
+ "List function triggers in the workspace. Triggers define how and when compute functions are executed (on-demand, event-driven, scheduled, webhook).",
44
+ {
45
+ executionType: z
46
+ .enum(["on-demand", "event-driven", "scheduled", "webhook"])
47
+ .optional()
48
+ .describe("Filter by trigger execution type"),
49
+ page: z.number().optional().describe("Page number"),
50
+ limit: z.number().optional().describe("Results per page"),
51
+ },
52
+ async ({ executionType, page, limit }) => {
53
+ try {
54
+ const options: Record<string, any> = {};
55
+ if (executionType) options.executionType = executionType;
56
+ if (page !== undefined) options.page = page;
57
+ if (limit !== undefined) options.limit = limit;
58
+
59
+ const result = await sdk.triggers.listAll(
60
+ Object.keys(options).length > 0 ? options : undefined
61
+ );
62
+ return {
63
+ content: [
64
+ { type: "text", text: JSON.stringify(result.data, null, 2) },
65
+ ],
66
+ };
67
+ } catch (error: any) {
68
+ return {
69
+ content: [
70
+ {
71
+ type: "text",
72
+ text: `Error listing triggers: ${error.message}`,
73
+ },
74
+ ],
75
+ isError: true,
76
+ };
77
+ }
78
+ }
79
+ );
80
+
81
+ server.tool(
82
+ "invoke_trigger",
83
+ "Invoke an on-demand function trigger by ID. Optionally pass a custom payload. Returns the queued job ID.",
84
+ {
85
+ triggerId: z.string().describe("The trigger ID (UUID) to invoke"),
86
+ payload: z
87
+ .record(z.string(), z.any())
88
+ .optional()
89
+ .describe("Custom payload to pass to the function execution"),
90
+ },
91
+ async ({ triggerId, payload }) => {
92
+ try {
93
+ const options = payload ? { payload } : undefined;
94
+ const result = await sdk.triggers.invoke(triggerId, options);
95
+ return {
96
+ content: [
97
+ {
98
+ type: "text",
99
+ text: JSON.stringify(
100
+ { jobId: result.data, message: "Trigger invoked successfully" },
101
+ null,
102
+ 2
103
+ ),
104
+ },
105
+ ],
106
+ };
107
+ } catch (error: any) {
108
+ return {
109
+ content: [
110
+ {
111
+ type: "text",
112
+ text: `Error invoking trigger '${triggerId}': ${error.message}`,
113
+ },
114
+ ],
115
+ isError: true,
116
+ };
117
+ }
118
+ }
119
+ );
120
+ }
@@ -0,0 +1,209 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { CentraliSDK } from "@centrali-io/centrali-sdk";
3
+ import { z } from "zod";
4
+
5
+ export function registerRecordTools(server: McpServer, sdk: CentraliSDK) {
6
+ server.tool(
7
+ "query_records",
8
+ "Query records from a structure with optional filters, sorting, and pagination. Filters use 'data.' prefix for custom fields and bracket notation for operators (e.g., 'data.status': 'active', 'data.price[lte]': 100).",
9
+ {
10
+ recordSlug: z
11
+ .string()
12
+ .describe("The structure's record slug (e.g., 'orders')"),
13
+ filters: z
14
+ .record(z.string(), z.any())
15
+ .optional()
16
+ .describe(
17
+ "Filter object with keys like 'data.fieldName' or 'data.fieldName[operator]'. Operators: eq, ne, gt, gte, lt, lte, in, nin, contains, startswith, endswith"
18
+ ),
19
+ sort: z
20
+ .string()
21
+ .optional()
22
+ .describe(
23
+ "Sort field with optional '-' prefix for descending (e.g., '-createdAt')"
24
+ ),
25
+ page: z.number().optional().describe("Page number (1-indexed, default: 1)"),
26
+ pageSize: z
27
+ .number()
28
+ .optional()
29
+ .describe("Records per page (default: 50, max: 500)"),
30
+ expand: z
31
+ .string()
32
+ .optional()
33
+ .describe(
34
+ "Comma-separated reference fields to expand (e.g., 'customer,product')"
35
+ ),
36
+ },
37
+ async ({ recordSlug, filters, sort, page, pageSize, expand }) => {
38
+ try {
39
+ const params: Record<string, any> = {
40
+ ...filters,
41
+ sort,
42
+ page,
43
+ pageSize,
44
+ expand,
45
+ };
46
+ // Remove undefined values
47
+ Object.keys(params).forEach(
48
+ (key) => params[key] === undefined && delete params[key]
49
+ );
50
+
51
+ const result = await sdk.queryRecords(recordSlug, params);
52
+ return {
53
+ content: [
54
+ { type: "text", text: JSON.stringify(result, null, 2) },
55
+ ],
56
+ };
57
+ } catch (error: any) {
58
+ return {
59
+ content: [
60
+ {
61
+ type: "text",
62
+ text: `Error querying records from '${recordSlug}': ${error.message}`,
63
+ },
64
+ ],
65
+ isError: true,
66
+ };
67
+ }
68
+ }
69
+ );
70
+
71
+ server.tool(
72
+ "get_record",
73
+ "Get a single record by its ID from a structure. Optionally expand reference fields.",
74
+ {
75
+ recordSlug: z.string().describe("The structure's record slug"),
76
+ id: z.string().describe("The record ID (UUID)"),
77
+ expand: z
78
+ .string()
79
+ .optional()
80
+ .describe("Comma-separated reference fields to expand"),
81
+ },
82
+ async ({ recordSlug, id, expand }) => {
83
+ try {
84
+ const options = expand ? { expand } : undefined;
85
+ const result = await sdk.getRecord(recordSlug, id, options);
86
+ return {
87
+ content: [
88
+ { type: "text", text: JSON.stringify(result.data, null, 2) },
89
+ ],
90
+ };
91
+ } catch (error: any) {
92
+ return {
93
+ content: [
94
+ {
95
+ type: "text",
96
+ text: `Error getting record '${id}' from '${recordSlug}': ${error.message}`,
97
+ },
98
+ ],
99
+ isError: true,
100
+ };
101
+ }
102
+ }
103
+ );
104
+
105
+ server.tool(
106
+ "create_record",
107
+ "Create a new record in a structure. Pass the record data as a JSON object with field names matching the structure's properties.",
108
+ {
109
+ recordSlug: z
110
+ .string()
111
+ .describe("The structure's record slug (e.g., 'orders')"),
112
+ data: z
113
+ .record(z.string(), z.any())
114
+ .describe("The record data object (field names as keys)"),
115
+ },
116
+ async ({ recordSlug, data }) => {
117
+ try {
118
+ const result = await sdk.createRecord(recordSlug, data);
119
+ return {
120
+ content: [
121
+ { type: "text", text: JSON.stringify(result.data, null, 2) },
122
+ ],
123
+ };
124
+ } catch (error: any) {
125
+ return {
126
+ content: [
127
+ {
128
+ type: "text",
129
+ text: `Error creating record in '${recordSlug}': ${error.message}`,
130
+ },
131
+ ],
132
+ isError: true,
133
+ };
134
+ }
135
+ }
136
+ );
137
+
138
+ server.tool(
139
+ "update_record",
140
+ "Update an existing record by ID. Only include the fields you want to change.",
141
+ {
142
+ recordSlug: z.string().describe("The structure's record slug"),
143
+ id: z.string().describe("The record ID (UUID) to update"),
144
+ data: z
145
+ .record(z.string(), z.any())
146
+ .describe("Object with fields to update (partial update)"),
147
+ },
148
+ async ({ recordSlug, id, data }) => {
149
+ try {
150
+ const result = await sdk.updateRecord(recordSlug, id, data);
151
+ return {
152
+ content: [
153
+ { type: "text", text: JSON.stringify(result.data, null, 2) },
154
+ ],
155
+ };
156
+ } catch (error: any) {
157
+ return {
158
+ content: [
159
+ {
160
+ type: "text",
161
+ text: `Error updating record '${id}' in '${recordSlug}': ${error.message}`,
162
+ },
163
+ ],
164
+ isError: true,
165
+ };
166
+ }
167
+ }
168
+ );
169
+
170
+ server.tool(
171
+ "delete_record",
172
+ "Delete a record by ID. Performs a soft delete by default (can be restored). Set hard=true for permanent deletion.",
173
+ {
174
+ recordSlug: z.string().describe("The structure's record slug"),
175
+ id: z.string().describe("The record ID (UUID) to delete"),
176
+ hard: z
177
+ .boolean()
178
+ .optional()
179
+ .describe(
180
+ "If true, permanently delete. Default: false (soft delete, can be restored)"
181
+ ),
182
+ },
183
+ async ({ recordSlug, id, hard }) => {
184
+ try {
185
+ const options = hard !== undefined ? { hard } : undefined;
186
+ await sdk.deleteRecord(recordSlug, id, options);
187
+ const deleteType = hard ? "permanently deleted" : "soft-deleted";
188
+ return {
189
+ content: [
190
+ {
191
+ type: "text",
192
+ text: `Record '${id}' ${deleteType} from '${recordSlug}'.`,
193
+ },
194
+ ],
195
+ };
196
+ } catch (error: any) {
197
+ return {
198
+ content: [
199
+ {
200
+ type: "text",
201
+ text: `Error deleting record '${id}' from '${recordSlug}': ${error.message}`,
202
+ },
203
+ ],
204
+ isError: true,
205
+ };
206
+ }
207
+ }
208
+ );
209
+ }
@@ -0,0 +1,50 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { CentraliSDK } from "@centrali-io/centrali-sdk";
3
+ import { z } from "zod";
4
+
5
+ export function registerSearchTools(server: McpServer, sdk: CentraliSDK) {
6
+ server.tool(
7
+ "search_records",
8
+ "Full-text search across records in the workspace. Powered by Meilisearch. Optionally filter by structure(s) and limit results.",
9
+ {
10
+ query: z.string().describe("The search query string"),
11
+ structures: z
12
+ .union([z.string(), z.array(z.string())])
13
+ .optional()
14
+ .describe(
15
+ "Filter by structure slug(s). Single slug string or array of slugs"
16
+ ),
17
+ limit: z
18
+ .number()
19
+ .optional()
20
+ .describe("Maximum results to return (default: 20, max: 100)"),
21
+ },
22
+ async ({ query, structures, limit }) => {
23
+ try {
24
+ const options: { structures?: string | string[]; limit?: number } = {};
25
+ if (structures) options.structures = structures;
26
+ if (limit) options.limit = limit;
27
+
28
+ const result = await sdk.search(
29
+ query,
30
+ Object.keys(options).length > 0 ? options : undefined
31
+ );
32
+ return {
33
+ content: [
34
+ { type: "text", text: JSON.stringify(result.data, null, 2) },
35
+ ],
36
+ };
37
+ } catch (error: any) {
38
+ return {
39
+ content: [
40
+ {
41
+ type: "text",
42
+ text: `Error searching records: ${error.message}`,
43
+ },
44
+ ],
45
+ isError: true,
46
+ };
47
+ }
48
+ }
49
+ );
50
+ }
@@ -0,0 +1,82 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { CentraliSDK } from "@centrali-io/centrali-sdk";
3
+ import { z } from "zod";
4
+
5
+ export function registerSmartQueryTools(server: McpServer, sdk: CentraliSDK) {
6
+ server.tool(
7
+ "list_smart_queries",
8
+ "List smart queries. Smart queries are reusable, parameterized queries defined in the Centrali console. Optionally filter by structure slug.",
9
+ {
10
+ recordSlug: z
11
+ .string()
12
+ .optional()
13
+ .describe(
14
+ "Filter by structure slug. If omitted, lists all smart queries in the workspace"
15
+ ),
16
+ },
17
+ async ({ recordSlug }) => {
18
+ try {
19
+ const result = recordSlug
20
+ ? await sdk.smartQueries.list(recordSlug)
21
+ : await sdk.smartQueries.listAll();
22
+ return {
23
+ content: [
24
+ { type: "text", text: JSON.stringify(result.data, null, 2) },
25
+ ],
26
+ };
27
+ } catch (error: any) {
28
+ return {
29
+ content: [
30
+ {
31
+ type: "text",
32
+ text: `Error listing smart queries: ${error.message}`,
33
+ },
34
+ ],
35
+ isError: true,
36
+ };
37
+ }
38
+ }
39
+ );
40
+
41
+ server.tool(
42
+ "execute_smart_query",
43
+ "Execute a smart query by ID and return the results. Smart queries can have parameterized variables using {{variableName}} syntax.",
44
+ {
45
+ recordSlug: z
46
+ .string()
47
+ .describe("The structure's record slug the query belongs to"),
48
+ queryId: z.string().describe("The smart query ID (UUID) to execute"),
49
+ variables: z
50
+ .record(z.string(), z.string())
51
+ .optional()
52
+ .describe(
53
+ "Variables to substitute in the query (key-value pairs matching {{variableName}} placeholders)"
54
+ ),
55
+ },
56
+ async ({ recordSlug, queryId, variables }) => {
57
+ try {
58
+ const options = variables ? { variables } : undefined;
59
+ const result = await sdk.smartQueries.execute(
60
+ recordSlug,
61
+ queryId,
62
+ options
63
+ );
64
+ return {
65
+ content: [
66
+ { type: "text", text: JSON.stringify(result.data, null, 2) },
67
+ ],
68
+ };
69
+ } catch (error: any) {
70
+ return {
71
+ content: [
72
+ {
73
+ type: "text",
74
+ text: `Error executing smart query '${queryId}': ${error.message}`,
75
+ },
76
+ ],
77
+ isError: true,
78
+ };
79
+ }
80
+ }
81
+ );
82
+ }