@centrali-io/centrali-mcp 4.4.5 → 4.4.7
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 +147 -14
- package/dist/index.js +5 -1
- package/dist/tools/auth-providers.d.ts +3 -0
- package/dist/tools/auth-providers.js +267 -0
- package/dist/tools/compute.d.ts +1 -1
- package/dist/tools/compute.js +184 -7
- package/dist/tools/describe.js +537 -9
- package/dist/tools/insights.js +1 -1
- package/dist/tools/orchestrations.js +9 -9
- package/dist/tools/pages.js +31 -12
- package/dist/tools/service-accounts.d.ts +3 -0
- package/dist/tools/service-accounts.js +856 -0
- package/package.json +2 -2
- package/src/index.ts +5 -1
- package/src/tools/auth-providers.ts +290 -0
- package/src/tools/compute.ts +202 -7
- package/src/tools/describe.ts +560 -9
- package/src/tools/insights.ts +1 -1
- package/src/tools/orchestrations.ts +9 -9
- package/src/tools/pages.ts +27 -9
- package/src/tools/service-accounts.ts +1051 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@centrali-io/centrali-mcp",
|
|
3
|
-
"version": "4.4.
|
|
3
|
+
"version": "4.4.7",
|
|
4
4
|
"description": "Centrali MCP Server - AI assistant integration for Centrali workspaces",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"author": "Blueinit",
|
|
26
26
|
"license": "ISC",
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@centrali-io/centrali-sdk": "^4.
|
|
28
|
+
"@centrali-io/centrali-sdk": "^4.4.7",
|
|
29
29
|
"@modelcontextprotocol/sdk": "^1.12.1"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,8 @@ import { registerInsightTools } from "./tools/insights.js";
|
|
|
13
13
|
import { registerValidationTools } from "./tools/validation.js";
|
|
14
14
|
import { registerPageTools } from "./tools/pages.js";
|
|
15
15
|
import { registerDescribeTools } from "./tools/describe.js";
|
|
16
|
+
import { registerAuthProviderTools } from "./tools/auth-providers.js";
|
|
17
|
+
import { registerServiceAccountTools } from "./tools/service-accounts.js";
|
|
16
18
|
import { registerStructureResources, registerCollectionResources } from "./resources/structures.js";
|
|
17
19
|
|
|
18
20
|
function getRequiredEnv(name: string): string {
|
|
@@ -47,12 +49,14 @@ async function main() {
|
|
|
47
49
|
registerStructureTools(server, sdk);
|
|
48
50
|
registerRecordTools(server, sdk);
|
|
49
51
|
registerSearchTools(server, sdk);
|
|
50
|
-
registerComputeTools(server, sdk);
|
|
52
|
+
registerComputeTools(server, sdk, baseUrl, workspaceId);
|
|
51
53
|
registerSmartQueryTools(server, sdk);
|
|
52
54
|
registerOrchestrationTools(server, sdk);
|
|
53
55
|
registerInsightTools(server, sdk);
|
|
54
56
|
registerValidationTools(server, sdk);
|
|
55
57
|
registerPageTools(server, sdk, baseUrl, workspaceId);
|
|
58
|
+
registerAuthProviderTools(server, sdk, baseUrl, workspaceId);
|
|
59
|
+
registerServiceAccountTools(server, sdk, baseUrl, workspaceId, clientId);
|
|
56
60
|
registerDescribeTools(server);
|
|
57
61
|
|
|
58
62
|
// Register resources
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { CentraliSDK } from "@centrali-io/centrali-sdk";
|
|
3
|
+
import axios, { AxiosInstance } from "axios";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Ensures the SDK has a valid token by making a lightweight SDK call if needed.
|
|
8
|
+
*/
|
|
9
|
+
async function ensureToken(sdk: CentraliSDK): Promise<string | null> {
|
|
10
|
+
let token = sdk.getToken();
|
|
11
|
+
if (token) return token;
|
|
12
|
+
try {
|
|
13
|
+
await sdk.functions.list({ limit: 1 });
|
|
14
|
+
} catch {
|
|
15
|
+
// Ignore — we only need the token refresh side effect
|
|
16
|
+
}
|
|
17
|
+
return sdk.getToken();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Creates an axios instance for the IAM external auth provider API.
|
|
22
|
+
*/
|
|
23
|
+
function createIamClient(sdk: CentraliSDK, centraliUrl: string, workspaceId: string): AxiosInstance {
|
|
24
|
+
const url = new URL(centraliUrl);
|
|
25
|
+
const hostname = url.hostname.startsWith("auth.")
|
|
26
|
+
? url.hostname
|
|
27
|
+
: `auth.${url.hostname.replace(/^api\./, '')}`;
|
|
28
|
+
const baseURL = `${url.protocol}//${hostname}/workspace/${workspaceId}/api/v1/external-auth-providers`;
|
|
29
|
+
|
|
30
|
+
const client = axios.create({ baseURL });
|
|
31
|
+
|
|
32
|
+
client.interceptors.request.use(async (config) => {
|
|
33
|
+
const token = await ensureToken(sdk);
|
|
34
|
+
if (token) {
|
|
35
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
36
|
+
}
|
|
37
|
+
return config;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
client.interceptors.response.use(
|
|
41
|
+
(response) => response,
|
|
42
|
+
async (error) => {
|
|
43
|
+
const originalRequest = error.config;
|
|
44
|
+
const isAuthError = error.response?.status === 401 || error.response?.status === 403;
|
|
45
|
+
|
|
46
|
+
if (isAuthError && !originalRequest._hasRetried) {
|
|
47
|
+
originalRequest._hasRetried = true;
|
|
48
|
+
try {
|
|
49
|
+
await sdk.functions.list({ limit: 1 });
|
|
50
|
+
} catch { /* token refresh side effect */ }
|
|
51
|
+
|
|
52
|
+
const token = sdk.getToken();
|
|
53
|
+
if (token) {
|
|
54
|
+
originalRequest.headers.Authorization = `Bearer ${token}`;
|
|
55
|
+
return client.request(originalRequest);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return Promise.reject(error);
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return client;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function formatError(error: unknown, context: string): string {
|
|
66
|
+
if (error && typeof error === "object") {
|
|
67
|
+
const e = error as Record<string, any>;
|
|
68
|
+
if (e.response?.data) {
|
|
69
|
+
const d = e.response.data;
|
|
70
|
+
const code = d.code ?? d.error?.code ?? e.response.status ?? "ERROR";
|
|
71
|
+
const message = d.message ?? d.error?.message ?? JSON.stringify(d);
|
|
72
|
+
return `Error ${context}: [${code}] ${message}`;
|
|
73
|
+
}
|
|
74
|
+
if ("message" in e) {
|
|
75
|
+
return `Error ${context}: ${e.message}`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return `Error ${context}: ${error instanceof Error ? error.message : String(error)}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const ClaimMappingZod = z.object({
|
|
82
|
+
attribute: z.string().describe("Attribute name used in policies (automatically prefixed with ext_). Must start with a letter, alphanumeric + underscores only. Example: 'role' becomes ext_role in policies."),
|
|
83
|
+
jwtPath: z.string().describe("Dot-notation path to extract from the JWT payload. Examples: 'org_role', 'metadata.plan', 'organization.role'"),
|
|
84
|
+
required: z.boolean().optional().describe("If true, token validation fails when this claim is missing (default: false)"),
|
|
85
|
+
defaultValue: z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]).optional().describe("Fallback value when claim is not present in the JWT"),
|
|
86
|
+
transform: z.enum(["lowercase", "uppercase", "string", "boolean", "array"]).optional().describe("Transform applied to the extracted value before use in policies"),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
export function registerAuthProviderTools(server: McpServer, sdk: CentraliSDK, centraliUrl: string, workspaceId: string) {
|
|
90
|
+
const getClient = () => createIamClient(sdk, centraliUrl, workspaceId);
|
|
91
|
+
|
|
92
|
+
// ── List ──────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
server.tool(
|
|
95
|
+
"list_auth_providers",
|
|
96
|
+
"List external auth providers configured in the workspace. These are identity providers (Clerk, Auth0, Okta, etc.) that can issue JWTs accepted by Centrali for BYOT (Bring Your Own Token) authorization.",
|
|
97
|
+
{
|
|
98
|
+
includeInactive: z.boolean().optional().describe("Include inactive providers (default: false)"),
|
|
99
|
+
},
|
|
100
|
+
async ({ includeInactive }) => {
|
|
101
|
+
try {
|
|
102
|
+
const params: Record<string, any> = {};
|
|
103
|
+
if (includeInactive) params.includeInactive = true;
|
|
104
|
+
const result = await getClient().get("/", { params });
|
|
105
|
+
return {
|
|
106
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
|
|
107
|
+
};
|
|
108
|
+
} catch (error: unknown) {
|
|
109
|
+
return {
|
|
110
|
+
content: [{ type: "text", text: formatError(error, "listing auth providers") }],
|
|
111
|
+
isError: true,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// ── Get ───────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
server.tool(
|
|
120
|
+
"get_auth_provider",
|
|
121
|
+
"Get details of an external auth provider by ID, including claim mappings and JWKS configuration.",
|
|
122
|
+
{
|
|
123
|
+
providerId: z.string().describe("The provider ID (UUID)"),
|
|
124
|
+
},
|
|
125
|
+
async ({ providerId }) => {
|
|
126
|
+
try {
|
|
127
|
+
const result = await getClient().get(`/${providerId}`);
|
|
128
|
+
return {
|
|
129
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
|
|
130
|
+
};
|
|
131
|
+
} catch (error: unknown) {
|
|
132
|
+
return {
|
|
133
|
+
content: [{ type: "text", text: formatError(error, `getting auth provider '${providerId}'`) }],
|
|
134
|
+
isError: true,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// ── Create ────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
server.tool(
|
|
143
|
+
"create_auth_provider",
|
|
144
|
+
"Create an external auth provider for BYOT (Bring Your Own Token). This lets your app's users authenticate with Clerk, Auth0, Okta, or any OIDC provider, and Centrali validates their JWTs and extracts claims for authorization policies. Call describe_auth_providers for the full setup guide.",
|
|
145
|
+
{
|
|
146
|
+
name: z.string().describe("Display name (e.g., 'Production Clerk')"),
|
|
147
|
+
slug: z.string().describe("URL-safe unique slug (lowercase, hyphens allowed, e.g., 'production-clerk')"),
|
|
148
|
+
providerType: z.enum(["oidc", "clerk", "auth0", "keycloak", "okta", "custom"]).describe("Identity provider type. Use 'clerk', 'auth0', 'okta', or 'keycloak' for built-in support, 'oidc' for any OIDC-compliant provider, or 'custom' for a custom JWT issuer."),
|
|
149
|
+
issuer: z.string().describe("The JWT issuer URL. Centrali auto-discovers JWKS from this. Examples: 'https://clerk.your-domain.com' for Clerk, 'https://your-tenant.auth0.com/' for Auth0, 'https://your-org.okta.com' for Okta."),
|
|
150
|
+
jwksUrl: z.string().optional().describe("Override JWKS URL if auto-discovery doesn't work (usually not needed for standard providers)"),
|
|
151
|
+
allowedAudiences: z.array(z.string()).optional().describe("JWT audience values to accept. If set, tokens without a matching 'aud' claim are rejected."),
|
|
152
|
+
clockSkewSeconds: z.number().optional().describe("Seconds of clock skew tolerance for token expiration (default: 60, max: 300)"),
|
|
153
|
+
allowedAlgorithms: z.array(z.enum(["RS256", "RS384", "RS512", "ES256", "ES384", "ES512"])).optional().describe("Accepted JWT signing algorithms (default: ['RS256'])"),
|
|
154
|
+
claimMappings: z.array(ClaimMappingZod).optional().describe("Map JWT claims to policy attributes. Each mapping extracts a value from the JWT and makes it available as ext_<attribute> in authorization policies. Example: { attribute: 'role', jwtPath: 'org_role' } makes ext_role available."),
|
|
155
|
+
allowedOrigins: z.array(z.string()).optional().describe("CORS origins allowed for browser requests with this provider's tokens"),
|
|
156
|
+
},
|
|
157
|
+
async ({ name, slug, providerType, issuer, jwksUrl, allowedAudiences, clockSkewSeconds, allowedAlgorithms, claimMappings, allowedOrigins }) => {
|
|
158
|
+
try {
|
|
159
|
+
const input: Record<string, any> = { name, slug, providerType, issuer };
|
|
160
|
+
if (jwksUrl !== undefined) input.jwksUrl = jwksUrl;
|
|
161
|
+
if (allowedAudiences !== undefined) input.allowedAudiences = allowedAudiences;
|
|
162
|
+
if (clockSkewSeconds !== undefined) input.clockSkewSeconds = clockSkewSeconds;
|
|
163
|
+
if (allowedAlgorithms !== undefined) input.allowedAlgorithms = allowedAlgorithms;
|
|
164
|
+
if (claimMappings !== undefined) input.claimMappings = claimMappings;
|
|
165
|
+
if (allowedOrigins !== undefined) input.allowedOrigins = allowedOrigins;
|
|
166
|
+
|
|
167
|
+
const result = await getClient().post("/", input);
|
|
168
|
+
return {
|
|
169
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
|
|
170
|
+
};
|
|
171
|
+
} catch (error: unknown) {
|
|
172
|
+
return {
|
|
173
|
+
content: [{ type: "text", text: formatError(error, `creating auth provider '${name}'`) }],
|
|
174
|
+
isError: true,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
// ── Update ────────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
server.tool(
|
|
183
|
+
"update_auth_provider",
|
|
184
|
+
"Update an external auth provider. Only include the fields you want to change. Use this to update claim mappings, allowed audiences, or deactivate a provider.",
|
|
185
|
+
{
|
|
186
|
+
providerId: z.string().describe("The provider ID (UUID) to update"),
|
|
187
|
+
name: z.string().optional().describe("Updated display name"),
|
|
188
|
+
jwksUrl: z.string().optional().describe("Updated JWKS URL"),
|
|
189
|
+
allowedAudiences: z.array(z.string()).optional().describe("Updated allowed audiences"),
|
|
190
|
+
clockSkewSeconds: z.number().optional().describe("Updated clock skew tolerance (0-300 seconds)"),
|
|
191
|
+
allowedAlgorithms: z.array(z.enum(["RS256", "RS384", "RS512", "ES256", "ES384", "ES512"])).optional().describe("Updated allowed algorithms"),
|
|
192
|
+
claimMappings: z.array(ClaimMappingZod).optional().describe("Updated claim mappings (replaces all existing mappings)"),
|
|
193
|
+
allowedOrigins: z.array(z.string()).optional().describe("Updated CORS origins"),
|
|
194
|
+
isActive: z.boolean().optional().describe("Set to false to deactivate the provider (tokens will be rejected)"),
|
|
195
|
+
},
|
|
196
|
+
async ({ providerId, name, jwksUrl, allowedAudiences, clockSkewSeconds, allowedAlgorithms, claimMappings, allowedOrigins, isActive }) => {
|
|
197
|
+
try {
|
|
198
|
+
const input: Record<string, any> = {};
|
|
199
|
+
if (name !== undefined) input.name = name;
|
|
200
|
+
if (jwksUrl !== undefined) input.jwksUrl = jwksUrl;
|
|
201
|
+
if (allowedAudiences !== undefined) input.allowedAudiences = allowedAudiences;
|
|
202
|
+
if (clockSkewSeconds !== undefined) input.clockSkewSeconds = clockSkewSeconds;
|
|
203
|
+
if (allowedAlgorithms !== undefined) input.allowedAlgorithms = allowedAlgorithms;
|
|
204
|
+
if (claimMappings !== undefined) input.claimMappings = claimMappings;
|
|
205
|
+
if (allowedOrigins !== undefined) input.allowedOrigins = allowedOrigins;
|
|
206
|
+
if (isActive !== undefined) input.isActive = isActive;
|
|
207
|
+
|
|
208
|
+
const result = await getClient().put(`/${providerId}`, input);
|
|
209
|
+
return {
|
|
210
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
|
|
211
|
+
};
|
|
212
|
+
} catch (error: unknown) {
|
|
213
|
+
return {
|
|
214
|
+
content: [{ type: "text", text: formatError(error, `updating auth provider '${providerId}'`) }],
|
|
215
|
+
isError: true,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// ── Delete ────────────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
server.tool(
|
|
224
|
+
"delete_auth_provider",
|
|
225
|
+
"Delete an external auth provider. Tokens from this provider will no longer be accepted.",
|
|
226
|
+
{
|
|
227
|
+
providerId: z.string().describe("The provider ID (UUID) to delete"),
|
|
228
|
+
},
|
|
229
|
+
async ({ providerId }) => {
|
|
230
|
+
try {
|
|
231
|
+
await getClient().delete(`/${providerId}`);
|
|
232
|
+
return {
|
|
233
|
+
content: [{ type: "text", text: `Auth provider '${providerId}' deleted successfully.` }],
|
|
234
|
+
};
|
|
235
|
+
} catch (error: unknown) {
|
|
236
|
+
return {
|
|
237
|
+
content: [{ type: "text", text: formatError(error, `deleting auth provider '${providerId}'`) }],
|
|
238
|
+
isError: true,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
// ── Test Claim Extraction ─────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
server.tool(
|
|
247
|
+
"test_auth_provider",
|
|
248
|
+
"Test claim extraction for an auth provider by passing a sample JWT. Decodes the token (without signature verification) and shows which claims would be extracted using the provider's configured mappings. Use this to validate your claim mappings before deploying.",
|
|
249
|
+
{
|
|
250
|
+
providerId: z.string().describe("The provider ID (UUID) to test against"),
|
|
251
|
+
token: z.string().describe("A sample JWT token from your identity provider"),
|
|
252
|
+
},
|
|
253
|
+
async ({ providerId, token }) => {
|
|
254
|
+
try {
|
|
255
|
+
const result = await getClient().post(`/${providerId}/test-extraction`, { token });
|
|
256
|
+
return {
|
|
257
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
|
|
258
|
+
};
|
|
259
|
+
} catch (error: unknown) {
|
|
260
|
+
return {
|
|
261
|
+
content: [{ type: "text", text: formatError(error, `testing auth provider '${providerId}'`) }],
|
|
262
|
+
isError: true,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// ── Refresh JWKS ──────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
server.tool(
|
|
271
|
+
"refresh_auth_provider_jwks",
|
|
272
|
+
"Force refresh the cached JWKS (JSON Web Key Set) for an auth provider. Use this after your identity provider rotates its signing keys.",
|
|
273
|
+
{
|
|
274
|
+
providerId: z.string().describe("The provider ID (UUID)"),
|
|
275
|
+
},
|
|
276
|
+
async ({ providerId }) => {
|
|
277
|
+
try {
|
|
278
|
+
const result = await getClient().post(`/${providerId}/refresh-jwks`);
|
|
279
|
+
return {
|
|
280
|
+
content: [{ type: "text", text: JSON.stringify(result.data, null, 2) }],
|
|
281
|
+
};
|
|
282
|
+
} catch (error: unknown) {
|
|
283
|
+
return {
|
|
284
|
+
content: [{ type: "text", text: formatError(error, `refreshing JWKS for auth provider '${providerId}'`) }],
|
|
285
|
+
isError: true,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
);
|
|
290
|
+
}
|
package/src/tools/compute.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { CentraliSDK } from "@centrali-io/centrali-sdk";
|
|
3
|
+
import axios from "axios";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
|
|
5
6
|
function formatError(error: unknown, context: string): string {
|
|
@@ -23,7 +24,19 @@ function formatError(error: unknown, context: string): string {
|
|
|
23
24
|
return `Error ${context}: ${error instanceof Error ? error.message : String(error)}`;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Ensures the SDK has a valid token.
|
|
29
|
+
*/
|
|
30
|
+
async function ensureToken(sdk: CentraliSDK): Promise<string | null> {
|
|
31
|
+
let token = sdk.getToken();
|
|
32
|
+
if (token) return token;
|
|
33
|
+
try {
|
|
34
|
+
await sdk.functions.list({ limit: 1 });
|
|
35
|
+
} catch { /* token refresh side effect */ }
|
|
36
|
+
return sdk.getToken();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function registerComputeTools(server: McpServer, sdk: CentraliSDK, centraliUrl: string, workspaceId: string) {
|
|
27
40
|
server.tool(
|
|
28
41
|
"list_functions",
|
|
29
42
|
"List all compute functions in the workspace. Compute functions are JavaScript code blocks that run server-side.",
|
|
@@ -61,10 +74,10 @@ export function registerComputeTools(server: McpServer, sdk: CentraliSDK) {
|
|
|
61
74
|
|
|
62
75
|
server.tool(
|
|
63
76
|
"list_triggers",
|
|
64
|
-
"List function triggers in the workspace. Triggers define how and when compute functions are executed (on-demand, event-driven, scheduled, http-trigger).",
|
|
77
|
+
"List function triggers in the workspace. Triggers define how and when compute functions are executed (on-demand, event-driven, scheduled, http-trigger, endpoint).",
|
|
65
78
|
{
|
|
66
79
|
executionType: z
|
|
67
|
-
.enum(["on-demand", "event-driven", "scheduled", "http-trigger"])
|
|
80
|
+
.enum(["on-demand", "event-driven", "scheduled", "http-trigger", "endpoint"])
|
|
68
81
|
.optional()
|
|
69
82
|
.describe("Filter by trigger execution type"),
|
|
70
83
|
page: z.number().optional().describe("Page number"),
|
|
@@ -118,7 +131,11 @@ export function registerComputeTools(server: McpServer, sdk: CentraliSDK) {
|
|
|
118
131
|
{
|
|
119
132
|
type: "text",
|
|
120
133
|
text: JSON.stringify(
|
|
121
|
-
{
|
|
134
|
+
{
|
|
135
|
+
jobId: result.data,
|
|
136
|
+
message: "Trigger invoked successfully. Execution is async — the function is running in the background.",
|
|
137
|
+
next_step: `To check the result, call get_compute_job_status with jobId='${result.data}'. It returns the job state (queued → running → completed | failed) and the return value or error.`,
|
|
138
|
+
},
|
|
122
139
|
null,
|
|
123
140
|
2
|
|
124
141
|
),
|
|
@@ -139,6 +156,43 @@ export function registerComputeTools(server: McpServer, sdk: CentraliSDK) {
|
|
|
139
156
|
}
|
|
140
157
|
);
|
|
141
158
|
|
|
159
|
+
server.tool(
|
|
160
|
+
"get_compute_job_status",
|
|
161
|
+
"Check the status of an async compute job by job ID. Returns the current state (queued, running, completed, failed), the return value on success, or the failure reason on error. Use this after invoke_trigger to poll for results.",
|
|
162
|
+
{
|
|
163
|
+
jobId: z.string().describe("The job ID returned by invoke_trigger"),
|
|
164
|
+
},
|
|
165
|
+
async ({ jobId }) => {
|
|
166
|
+
try {
|
|
167
|
+
const token = await ensureToken(sdk);
|
|
168
|
+
const url = new URL(centraliUrl);
|
|
169
|
+
const hostname = url.hostname.startsWith("api.")
|
|
170
|
+
? url.hostname
|
|
171
|
+
: `api.${url.hostname}`;
|
|
172
|
+
const apiUrl = `${url.protocol}//${hostname}/data/workspace/${workspaceId}/api/v1/jobs/compute/${jobId}`;
|
|
173
|
+
|
|
174
|
+
const result = await axios.get(apiUrl, {
|
|
175
|
+
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
176
|
+
});
|
|
177
|
+
return {
|
|
178
|
+
content: [
|
|
179
|
+
{ type: "text", text: JSON.stringify(result.data, null, 2) },
|
|
180
|
+
],
|
|
181
|
+
};
|
|
182
|
+
} catch (error: unknown) {
|
|
183
|
+
return {
|
|
184
|
+
content: [
|
|
185
|
+
{
|
|
186
|
+
type: "text",
|
|
187
|
+
text: formatError(error, `getting job status for '${jobId}'`),
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
isError: true,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
);
|
|
195
|
+
|
|
142
196
|
// ── Function CRUD tools ──────────────────────────────────────────
|
|
143
197
|
|
|
144
198
|
server.tool(
|
|
@@ -383,13 +437,13 @@ export function registerComputeTools(server: McpServer, sdk: CentraliSDK) {
|
|
|
383
437
|
name: z.string().describe("Display name for the trigger"),
|
|
384
438
|
functionId: z.string().describe("The compute function ID (UUID) to execute"),
|
|
385
439
|
executionType: z
|
|
386
|
-
.enum(["on-demand", "event-driven", "scheduled", "http-trigger"])
|
|
387
|
-
.describe("How the trigger fires: on-demand (manual), event-driven (data events), scheduled (cron),
|
|
440
|
+
.enum(["on-demand", "event-driven", "scheduled", "http-trigger", "endpoint"])
|
|
441
|
+
.describe("How the trigger fires: on-demand (manual), event-driven (data events), scheduled (cron), http-trigger (external HTTP POST), or endpoint (synchronous HTTP API — returns response inline)"),
|
|
388
442
|
description: z.string().optional().describe("Optional description"),
|
|
389
443
|
triggerMetadata: z
|
|
390
444
|
.record(z.string(), z.any())
|
|
391
445
|
.optional()
|
|
392
|
-
.describe("Type-specific configuration. For event-driven: { eventType, recordSlug }
|
|
446
|
+
.describe("Type-specific configuration. For event-driven: { eventType, recordSlug }. For scheduled: { scheduleType, cronExpression, timezone }. For http-trigger: auto-generated URL. For endpoint: { path, allowedMethods?, timeoutMs?, auth? } where path is URL-safe (e.g., 'create-order'), allowedMethods defaults to ['POST'], timeoutMs 1000-30000 (default 30000), auth is { mode: 'bearer'|'public'|'apiKey'|'hmac' }."),
|
|
393
447
|
enabled: z.boolean().optional().describe("Whether the trigger is enabled (default: true)"),
|
|
394
448
|
},
|
|
395
449
|
async ({ name, functionId, executionType, description, triggerMetadata, enabled }) => {
|
|
@@ -547,6 +601,147 @@ export function registerComputeTools(server: McpServer, sdk: CentraliSDK) {
|
|
|
547
601
|
}
|
|
548
602
|
);
|
|
549
603
|
|
|
604
|
+
// ── Function Runs tools ────────────────────────────────────────────
|
|
605
|
+
|
|
606
|
+
server.tool(
|
|
607
|
+
"get_function_run",
|
|
608
|
+
"Get a function run by ID. Returns status, output, error, timing, and execution metadata. Use this to check the result of an async trigger invocation or test execution.",
|
|
609
|
+
{
|
|
610
|
+
runId: z.string().describe("The function run ID (UUID)"),
|
|
611
|
+
},
|
|
612
|
+
async ({ runId }) => {
|
|
613
|
+
try {
|
|
614
|
+
const result = await sdk.runs.get(runId);
|
|
615
|
+
return {
|
|
616
|
+
content: [
|
|
617
|
+
{ type: "text", text: JSON.stringify(result.data, null, 2) },
|
|
618
|
+
],
|
|
619
|
+
};
|
|
620
|
+
} catch (error: unknown) {
|
|
621
|
+
return {
|
|
622
|
+
content: [
|
|
623
|
+
{
|
|
624
|
+
type: "text",
|
|
625
|
+
text: formatError(error, `getting function run '${runId}'`),
|
|
626
|
+
},
|
|
627
|
+
],
|
|
628
|
+
isError: true,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
server.tool(
|
|
635
|
+
"list_function_runs",
|
|
636
|
+
"List function runs filtered by trigger ID or function ID. Returns execution history with status, timing, and errors. Useful for checking whether a trigger invocation completed and what it returned.",
|
|
637
|
+
{
|
|
638
|
+
triggerId: z.string().optional().describe("Filter runs by trigger ID (UUID)"),
|
|
639
|
+
functionId: z.string().optional().describe("Filter runs by function ID (UUID)"),
|
|
640
|
+
status: z
|
|
641
|
+
.enum(["pending", "running", "completed", "failure", "timeout"])
|
|
642
|
+
.optional()
|
|
643
|
+
.describe("Filter by run status"),
|
|
644
|
+
page: z.number().optional().describe("Page number"),
|
|
645
|
+
limit: z.number().optional().describe("Results per page"),
|
|
646
|
+
},
|
|
647
|
+
async ({ triggerId, functionId, status, page, limit }) => {
|
|
648
|
+
try {
|
|
649
|
+
if (!triggerId && !functionId) {
|
|
650
|
+
return {
|
|
651
|
+
content: [
|
|
652
|
+
{
|
|
653
|
+
type: "text",
|
|
654
|
+
text: "Error: provide either triggerId or functionId to list runs.",
|
|
655
|
+
},
|
|
656
|
+
],
|
|
657
|
+
isError: true,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const options: Record<string, any> = {};
|
|
662
|
+
if (status) options.status = status;
|
|
663
|
+
if (page !== undefined) options.page = page;
|
|
664
|
+
if (limit !== undefined) options.limit = limit;
|
|
665
|
+
|
|
666
|
+
const opts = Object.keys(options).length > 0 ? options : undefined;
|
|
667
|
+
const result = triggerId
|
|
668
|
+
? await sdk.runs.listByTrigger(triggerId, opts)
|
|
669
|
+
: await sdk.runs.listByFunction(functionId!, opts);
|
|
670
|
+
|
|
671
|
+
return {
|
|
672
|
+
content: [
|
|
673
|
+
{ type: "text", text: JSON.stringify(result.data, null, 2) },
|
|
674
|
+
],
|
|
675
|
+
};
|
|
676
|
+
} catch (error: unknown) {
|
|
677
|
+
return {
|
|
678
|
+
content: [
|
|
679
|
+
{
|
|
680
|
+
type: "text",
|
|
681
|
+
text: formatError(error, `listing function runs`),
|
|
682
|
+
},
|
|
683
|
+
],
|
|
684
|
+
isError: true,
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
// ── Endpoint Trigger (Sync Execution) ─────────────────────────────
|
|
691
|
+
|
|
692
|
+
server.tool(
|
|
693
|
+
"invoke_endpoint",
|
|
694
|
+
"Invoke a compute endpoint trigger by path. The function executes synchronously — Centrali waits for the function to complete and returns its output directly in the response. No polling needed. Max execution time: 30 seconds (configurable via triggerMetadata.timeoutMs, range 1–30s). If the function exceeds the timeout, returns 504. Endpoint triggers must be created first with executionType='endpoint'. Use this for real-time API responses; use invoke_trigger for long-running background work that doesn't need an immediate response.",
|
|
695
|
+
{
|
|
696
|
+
path: z.string().describe("The endpoint path (e.g., 'create-order', 'webhook/shipments'). This is set in the trigger's triggerMetadata.path."),
|
|
697
|
+
method: z.enum(["GET", "POST", "PUT", "DELETE", "PATCH"]).optional().describe("HTTP method (default: POST). Must be in the trigger's allowedMethods."),
|
|
698
|
+
payload: z.record(z.string(), z.any()).optional().describe("Request body payload (sent as JSON)"),
|
|
699
|
+
headers: z.record(z.string(), z.string()).optional().describe("Additional headers (e.g., X-API-Key for apiKey auth)"),
|
|
700
|
+
},
|
|
701
|
+
async ({ path, method, payload, headers: extraHeaders }) => {
|
|
702
|
+
try {
|
|
703
|
+
const token = await ensureToken(sdk);
|
|
704
|
+
const url = new URL(centraliUrl);
|
|
705
|
+
const hostname = url.hostname.startsWith("api.")
|
|
706
|
+
? url.hostname
|
|
707
|
+
: `api.${url.hostname}`;
|
|
708
|
+
const apiUrl = `${url.protocol}//${hostname}/data/workspace/${workspaceId}/api/v1/endpoints/${path}`;
|
|
709
|
+
|
|
710
|
+
const reqHeaders: Record<string, string> = {};
|
|
711
|
+
if (token) reqHeaders.Authorization = `Bearer ${token}`;
|
|
712
|
+
if (extraHeaders) Object.assign(reqHeaders, extraHeaders);
|
|
713
|
+
|
|
714
|
+
const httpMethod = (method || "POST").toLowerCase();
|
|
715
|
+
const result = await axios({
|
|
716
|
+
method: httpMethod as any,
|
|
717
|
+
url: apiUrl,
|
|
718
|
+
data: ["get", "delete"].includes(httpMethod) ? undefined : (payload || {}),
|
|
719
|
+
headers: reqHeaders,
|
|
720
|
+
validateStatus: () => true, // Don't throw on non-2xx — return the function's response as-is
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
return {
|
|
724
|
+
content: [{
|
|
725
|
+
type: "text",
|
|
726
|
+
text: JSON.stringify({
|
|
727
|
+
status: result.status,
|
|
728
|
+
headers: {
|
|
729
|
+
"content-type": result.headers["content-type"],
|
|
730
|
+
"x-execution-id": result.headers["x-execution-id"],
|
|
731
|
+
},
|
|
732
|
+
body: result.data,
|
|
733
|
+
}, null, 2),
|
|
734
|
+
}],
|
|
735
|
+
};
|
|
736
|
+
} catch (error: unknown) {
|
|
737
|
+
return {
|
|
738
|
+
content: [{ type: "text", text: formatError(error, `invoking endpoint '${path}'`) }],
|
|
739
|
+
isError: true,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
);
|
|
744
|
+
|
|
550
745
|
// ── Allowed Domains tools ──────────────────────────────────────────
|
|
551
746
|
|
|
552
747
|
server.tool(
|