@nextsparkjs/core 0.1.0-beta.184 → 0.1.0-beta.185
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/hooks/useAuth.d.ts +74 -25
- package/dist/hooks/useAuth.d.ts.map +1 -1
- package/dist/hooks/useValidationSchemas.d.ts +7 -7
- package/dist/lib/api/auth/dual-auth.d.ts +7 -0
- package/dist/lib/api/auth/dual-auth.d.ts.map +1 -1
- package/dist/lib/api/auth/dual-auth.js +1 -1
- package/dist/lib/api/auth.d.ts +20 -3
- package/dist/lib/api/auth.d.ts.map +1 -1
- package/dist/lib/api/auth.js +37 -19
- package/dist/lib/api/entity/generic-handler.d.ts.map +1 -1
- package/dist/lib/api/entity/generic-handler.js +19 -22
- package/dist/lib/auth.d.ts +1 -3057
- package/dist/lib/auth.d.ts.map +1 -1
- package/dist/lib/billing/schema.d.ts +1 -1
- package/dist/lib/mcp/audit.d.ts +17 -0
- package/dist/lib/mcp/audit.d.ts.map +1 -0
- package/dist/lib/mcp/audit.js +48 -0
- package/dist/lib/mcp/engine.d.ts +28 -0
- package/dist/lib/mcp/engine.d.ts.map +1 -0
- package/dist/lib/mcp/engine.js +158 -0
- package/dist/lib/mcp/errors.d.ts +16 -0
- package/dist/lib/mcp/errors.d.ts.map +1 -0
- package/dist/lib/mcp/errors.js +66 -0
- package/dist/lib/mcp/executor.d.ts +15 -0
- package/dist/lib/mcp/executor.d.ts.map +1 -0
- package/dist/lib/mcp/executor.js +63 -0
- package/dist/lib/mcp/index.d.ts +9 -0
- package/dist/lib/mcp/index.d.ts.map +1 -0
- package/dist/lib/mcp/index.js +32 -0
- package/dist/lib/mcp/schema-builder.d.ts +45 -0
- package/dist/lib/mcp/schema-builder.d.ts.map +1 -0
- package/dist/lib/mcp/schema-builder.js +212 -0
- package/dist/lib/mcp/tool-generator.d.ts +31 -0
- package/dist/lib/mcp/tool-generator.d.ts.map +1 -0
- package/dist/lib/mcp/tool-generator.js +271 -0
- package/dist/lib/mcp/transport.d.ts +31 -0
- package/dist/lib/mcp/transport.d.ts.map +1 -0
- package/dist/lib/mcp/transport.js +58 -0
- package/dist/lib/mcp/types.d.ts +184 -0
- package/dist/lib/mcp/types.d.ts.map +1 -0
- package/dist/lib/mcp/types.js +11 -0
- package/dist/styles/classes.json +1 -1
- package/dist/styles/ui.css +1 -1
- package/dist/templates/app/api/mcp/route.ts +173 -0
- package/dist/templates/app/api/v1/api-keys/route.ts +15 -3
- package/dist/templates/app/layout.tsx +3 -0
- package/package.json +8 -3
- package/scripts/build/registry/__tests__/mcp-registry.test.mjs +64 -0
- package/scripts/build/registry/__tests__/scope-registry.test.mjs +88 -0
- package/scripts/build/registry/discovery/mcp-overrides.mjs +72 -0
- package/scripts/build/registry/discovery/mcp-overrides.test.mjs +86 -0
- package/scripts/build/registry/generators/mcp-registry.mjs +61 -0
- package/scripts/build/registry/generators/scope-registry.mjs +133 -88
- package/scripts/build/registry.mjs +8 -4
- package/templates/app/api/mcp/route.ts +173 -0
- package/templates/app/api/v1/api-keys/route.ts +15 -3
- package/templates/app/layout.tsx +3 -0
- package/tests/jest/__mocks__/@nextsparkjs/registries/scope-registry.ts +56 -6
- package/dist/lib/auth-client.d.ts +0 -3595
- package/dist/lib/auth-client.d.ts.map +0 -1
package/dist/lib/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/lib/auth.ts"],"names":[],"mappings":"AAUA,OAAO,EAAgF,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAC;AAavH;;;;;;;;;;;;;GAaG;AACH,wBAAsB,4BAA4B,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAQlF;AAED;;;;;;;;;GASG;AACH,wBAAsB,6BAA6B,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAEnF;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA0GD,eAAO,MAAM,IAAI
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/lib/auth.ts"],"names":[],"mappings":"AAUA,OAAO,EAAgF,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAC;AAavH;;;;;;;;;;;;;GAaG;AACH,wBAAsB,4BAA4B,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAQlF;AAED;;;;;;;;;GASG;AACH,wBAAsB,6BAA6B,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAEnF;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA0GD,eAAO,MAAM,IAAI,qEAkUf,CAAC;AAEH,MAAM,MAAM,OAAO,GAAG,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC;AACjD,MAAM,MAAM,WAAW,GAAG,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,GAAG;IAC1D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,kBAAkB,EAAE,QAAQ,EAAE,CAAC;CAC/C,CAAC;AAEF;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;IAC5B,IAAI,EAAE,WAAW,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAO9B"}
|
|
@@ -24,8 +24,8 @@ export declare const subscriptionStatusSchema: z.ZodEnum<{
|
|
|
24
24
|
canceled: "canceled";
|
|
25
25
|
}>;
|
|
26
26
|
export declare const paymentProviderSchema: z.ZodEnum<{
|
|
27
|
-
stripe: "stripe";
|
|
28
27
|
polar: "polar";
|
|
28
|
+
stripe: "stripe";
|
|
29
29
|
}>;
|
|
30
30
|
export declare const billingEventTypeSchema: z.ZodEnum<{
|
|
31
31
|
payment: "payment";
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP audit trail.
|
|
3
|
+
*
|
|
4
|
+
* The core's api_audit_log exists but the generic entity handlers never write
|
|
5
|
+
* to it directly for in-process invocations (logApiUsage only runs inside
|
|
6
|
+
* withApiAuth, which the MCP executor's synthesized requests don't go
|
|
7
|
+
* through). Every MCP tool call is recorded here so destructive actions by an
|
|
8
|
+
* autonomous agent are at least reconstructable — same insert shape as
|
|
9
|
+
* `logApiUsage` (packages/core/src/lib/api/helpers.ts), including passing the
|
|
10
|
+
* caller's userId as RLS context.
|
|
11
|
+
*
|
|
12
|
+
* Note for operators: revoking an API key can take up to 5 minutes to become
|
|
13
|
+
* effective (core apiKeyCache TTL) — the audit log covers that window.
|
|
14
|
+
*/
|
|
15
|
+
import type { AuditEntry } from './types';
|
|
16
|
+
export declare function auditToolCall(entry: AuditEntry): Promise<void>;
|
|
17
|
+
//# sourceMappingURL=audit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../../src/lib/mcp/audit.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAIzC,wBAAsB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA0CpE"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { mutateWithRLS } from "../db.js";
|
|
2
|
+
const MAX_SUMMARY_LENGTH = 4e3;
|
|
3
|
+
async function auditToolCall(entry) {
|
|
4
|
+
if (!entry.apiKeyId) {
|
|
5
|
+
console.warn("[mcp:audit] tool call without apiKeyId \u2014 skipping DB audit", {
|
|
6
|
+
tool: entry.tool,
|
|
7
|
+
userId: entry.userId
|
|
8
|
+
});
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const requestBody = {
|
|
12
|
+
tool: entry.tool,
|
|
13
|
+
entity: entry.entitySlug ?? null,
|
|
14
|
+
operation: entry.operation ?? null,
|
|
15
|
+
recordId: entry.recordId ?? null,
|
|
16
|
+
teamId: entry.teamId,
|
|
17
|
+
args: truncate(entry.requestSummary)
|
|
18
|
+
};
|
|
19
|
+
try {
|
|
20
|
+
await mutateWithRLS(
|
|
21
|
+
`INSERT INTO "api_audit_log"
|
|
22
|
+
("apiKeyId", "userId", endpoint, method, "statusCode", "ipAddress", "userAgent", "requestBody", "responseTime")
|
|
23
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
|
24
|
+
[
|
|
25
|
+
entry.apiKeyId,
|
|
26
|
+
entry.userId,
|
|
27
|
+
`mcp:${entry.tool}`,
|
|
28
|
+
(entry.operation ?? "workflow").toUpperCase(),
|
|
29
|
+
entry.statusCode,
|
|
30
|
+
"mcp",
|
|
31
|
+
"mcp-client",
|
|
32
|
+
JSON.stringify(requestBody),
|
|
33
|
+
entry.responseTimeMs
|
|
34
|
+
],
|
|
35
|
+
entry.userId
|
|
36
|
+
);
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error("[mcp:audit] failed to write api_audit_log entry:", error);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function truncate(args) {
|
|
42
|
+
const json = JSON.stringify(args);
|
|
43
|
+
if (json.length <= MAX_SUMMARY_LENGTH) return args;
|
|
44
|
+
return { truncated: true, preview: json.slice(0, MAX_SUMMARY_LENGTH) };
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
auditToolCall
|
|
48
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Engine — registry-driven server factory.
|
|
3
|
+
*
|
|
4
|
+
* createMcpEngine(registry, options) builds the full tool set from the entity
|
|
5
|
+
* registry (entities with access.api, minus reserved slugs) plus injected
|
|
6
|
+
* overrides and utility tools. The engine NEVER imports theme files: overrides
|
|
7
|
+
* and translations arrive via options — the default route template assembles
|
|
8
|
+
* them from the generated MCP_OVERRIDES registry.
|
|
9
|
+
*
|
|
10
|
+
* Stateless per request: handleJsonRpc creates an ephemeral McpServer bound
|
|
11
|
+
* to the request's ToolExecutionContext, processes the message(s), and tears
|
|
12
|
+
* everything down. Every tool call is audited.
|
|
13
|
+
*
|
|
14
|
+
* No separate authorization layer runs here: every generated tool's executor
|
|
15
|
+
* round-trips in-process through the real generic entity handlers, which
|
|
16
|
+
* already enforce scope + team-role permission + ownership/field guards for
|
|
17
|
+
* API-key auth (see tool-generator.ts).
|
|
18
|
+
*/
|
|
19
|
+
import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
|
|
20
|
+
import type { EntityConfig } from '../entities/types';
|
|
21
|
+
import type { EngineApi, McpEngineOptions, McpToolDefinition, ToolExecutionContext } from './types';
|
|
22
|
+
export interface McpEngine {
|
|
23
|
+
tools: McpToolDefinition[];
|
|
24
|
+
handleJsonRpc: (body: unknown, ctx: ToolExecutionContext) => Promise<JSONRPCMessage | JSONRPCMessage[] | null>;
|
|
25
|
+
api: EngineApi;
|
|
26
|
+
}
|
|
27
|
+
export declare function createMcpEngine(registry: Record<string, EntityConfig>, options: McpEngineOptions): McpEngine;
|
|
28
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../../src/lib/mcp/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAA;AAExE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAOrD,OAAO,KAAK,EACV,SAAS,EAET,gBAAgB,EAEhB,iBAAiB,EAEjB,oBAAoB,EACrB,MAAM,SAAS,CAAA;AAMhB,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,iBAAiB,EAAE,CAAA;IAC1B,aAAa,EAAE,CACb,IAAI,EAAE,OAAO,EACb,GAAG,EAAE,oBAAoB,KACtB,OAAO,CAAC,cAAc,GAAG,cAAc,EAAE,GAAG,IAAI,CAAC,CAAA;IACtD,GAAG,EAAE,SAAS,CAAA;CACf;AAED,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,EACtC,OAAO,EAAE,gBAAgB,GACxB,SAAS,CA8JX"}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { StatelessJsonRpcTransport } from "./transport.js";
|
|
4
|
+
import { generateEntityTools, normalizeDateFields } from "./tool-generator.js";
|
|
5
|
+
import { buildEntitySchemas } from "./schema-builder.js";
|
|
6
|
+
import { executeEntityOperation } from "./executor.js";
|
|
7
|
+
import { auditToolCall } from "./audit.js";
|
|
8
|
+
import { translateApiError } from "./errors.js";
|
|
9
|
+
const DEFAULT_OPERATIONS = ["list", "get", "create", "update", "delete"];
|
|
10
|
+
const DEFAULT_EXCLUDED_SLUGS = ["patterns"];
|
|
11
|
+
const DEFAULT_TIMEZONE = "UTC";
|
|
12
|
+
function createMcpEngine(registry, options) {
|
|
13
|
+
var _a, _b;
|
|
14
|
+
const executor = options.executor ?? executeEntityOperation;
|
|
15
|
+
const audit = options.audit ?? auditToolCall;
|
|
16
|
+
const excluded = new Set(options.excludeSlugs ?? DEFAULT_EXCLUDED_SLUGS);
|
|
17
|
+
const defaultOperations = options.defaultOperations ?? DEFAULT_OPERATIONS;
|
|
18
|
+
const overrides = options.overrides ?? {};
|
|
19
|
+
const normalizingExecutor = async (args, ctx) => {
|
|
20
|
+
var _a2;
|
|
21
|
+
const result = await executor(args, ctx);
|
|
22
|
+
if (!result.body.success || result.body.data === void 0) return result;
|
|
23
|
+
const entityConfig = registry[args.slug];
|
|
24
|
+
const dateFields = ((_a2 = entityConfig == null ? void 0 : entityConfig.fields) == null ? void 0 : _a2.filter((f) => f.type === "date").map((f) => f.name)) ?? [];
|
|
25
|
+
const override = overrides[args.slug];
|
|
26
|
+
const normalizeRow = (row) => normalizeDateFields(row, dateFields);
|
|
27
|
+
let data = Array.isArray(result.body.data) ? result.body.data.map(normalizeRow) : normalizeRow(result.body.data);
|
|
28
|
+
if (override == null ? void 0 : override.transformOutput) data = override.transformOutput(args.operation, data);
|
|
29
|
+
return { ...result, body: { ...result.body, data } };
|
|
30
|
+
};
|
|
31
|
+
const api = {
|
|
32
|
+
execute: normalizingExecutor,
|
|
33
|
+
entitySchemas: (slug) => {
|
|
34
|
+
const entityConfig = registry[slug];
|
|
35
|
+
if (!entityConfig) throw new Error(`Unknown entity for MCP schemas: ${slug}`);
|
|
36
|
+
return buildEntitySchemas(entityConfig, {
|
|
37
|
+
translateOption: options.translateOption,
|
|
38
|
+
override: overrides[slug]
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
toolPrefix: options.toolPrefix,
|
|
42
|
+
defaultTimezone: options.defaultTimezone ?? DEFAULT_TIMEZONE,
|
|
43
|
+
registry,
|
|
44
|
+
translateApiError
|
|
45
|
+
};
|
|
46
|
+
const tools = [];
|
|
47
|
+
for (const [slug, entityConfig] of Object.entries(registry)) {
|
|
48
|
+
if (excluded.has(slug)) continue;
|
|
49
|
+
if (!(entityConfig == null ? void 0 : entityConfig.enabled)) continue;
|
|
50
|
+
if (!((_a = entityConfig.access) == null ? void 0 : _a.api)) continue;
|
|
51
|
+
const override = overrides[slug];
|
|
52
|
+
if (override == null ? void 0 : override.exclude) continue;
|
|
53
|
+
const operations = defaultOperations.filter(
|
|
54
|
+
(op) => {
|
|
55
|
+
var _a2;
|
|
56
|
+
return !((_a2 = override == null ? void 0 : override.excludeOperations) == null ? void 0 : _a2.includes(op));
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
if (!operations.length) continue;
|
|
60
|
+
tools.push(
|
|
61
|
+
...generateEntityTools(entityConfig, operations, override, {
|
|
62
|
+
executor: normalizingExecutor,
|
|
63
|
+
toolPrefix: options.toolPrefix,
|
|
64
|
+
translateOption: options.translateOption,
|
|
65
|
+
presets: (_b = options.presets) == null ? void 0 : _b[slug]
|
|
66
|
+
})
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
for (const [slug, override] of Object.entries(overrides)) {
|
|
70
|
+
if (!override.extraTools) continue;
|
|
71
|
+
if (excluded.has(slug)) continue;
|
|
72
|
+
tools.push(...override.extraTools(api));
|
|
73
|
+
}
|
|
74
|
+
if (options.utilityTools) {
|
|
75
|
+
tools.push(...options.utilityTools(api));
|
|
76
|
+
}
|
|
77
|
+
assertUniqueNames(tools);
|
|
78
|
+
async function handleJsonRpc(body, ctx) {
|
|
79
|
+
const server = new McpServer(
|
|
80
|
+
options.serverInfo ?? { name: `${options.toolPrefix}-mcp`, version: "1.0.0" }
|
|
81
|
+
);
|
|
82
|
+
for (const tool of tools) {
|
|
83
|
+
server.registerTool(
|
|
84
|
+
tool.name,
|
|
85
|
+
{
|
|
86
|
+
title: tool.title,
|
|
87
|
+
description: tool.description,
|
|
88
|
+
// A strict ZodObject (not the raw shape): the SDK parses arguments
|
|
89
|
+
// with it and emits additionalProperties:false in tools/list, so a
|
|
90
|
+
// misspelled parameter is an error instead of being dropped
|
|
91
|
+
// silently. Cross-field rules still live in tool.strictSchema,
|
|
92
|
+
// which the handler applies.
|
|
93
|
+
inputSchema: z.object(tool.inputSchema).strict(),
|
|
94
|
+
annotations: tool.annotations
|
|
95
|
+
},
|
|
96
|
+
(async (args) => {
|
|
97
|
+
var _a2, _b2;
|
|
98
|
+
const startedAt = Date.now();
|
|
99
|
+
let result;
|
|
100
|
+
try {
|
|
101
|
+
result = await tool.handler(args ?? {}, ctx);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
result = {
|
|
104
|
+
content: [
|
|
105
|
+
{
|
|
106
|
+
type: "text",
|
|
107
|
+
text: `Error inesperado: ${error instanceof Error ? error.message : String(error)}`
|
|
108
|
+
}
|
|
109
|
+
],
|
|
110
|
+
isError: true
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
await audit({
|
|
114
|
+
apiKeyId: ctx.apiKeyId,
|
|
115
|
+
userId: ctx.userId,
|
|
116
|
+
tool: tool.name,
|
|
117
|
+
entitySlug: (_a2 = tool.meta) == null ? void 0 : _a2.entitySlug,
|
|
118
|
+
operation: (_b2 = tool.meta) == null ? void 0 : _b2.operation,
|
|
119
|
+
recordId: typeof (args == null ? void 0 : args.id) === "string" ? args.id : void 0,
|
|
120
|
+
teamId: ctx.teamId,
|
|
121
|
+
statusCode: result.isError ? 400 : 200,
|
|
122
|
+
requestSummary: args ?? {},
|
|
123
|
+
responseTimeMs: Date.now() - startedAt
|
|
124
|
+
});
|
|
125
|
+
return result;
|
|
126
|
+
})
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
const transport = new StatelessJsonRpcTransport();
|
|
130
|
+
await server.connect(transport);
|
|
131
|
+
try {
|
|
132
|
+
if (Array.isArray(body)) {
|
|
133
|
+
const responses = [];
|
|
134
|
+
for (const message of body) {
|
|
135
|
+
const response = await transport.handleMessage(message);
|
|
136
|
+
if (response) responses.push(response);
|
|
137
|
+
}
|
|
138
|
+
return responses.length ? responses : null;
|
|
139
|
+
}
|
|
140
|
+
return await transport.handleMessage(body);
|
|
141
|
+
} finally {
|
|
142
|
+
await transport.close();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { tools, handleJsonRpc, api };
|
|
146
|
+
}
|
|
147
|
+
function assertUniqueNames(tools) {
|
|
148
|
+
const seen = /* @__PURE__ */ new Set();
|
|
149
|
+
for (const tool of tools) {
|
|
150
|
+
if (seen.has(tool.name)) {
|
|
151
|
+
throw new Error(`Duplicate MCP tool name: ${tool.name}`);
|
|
152
|
+
}
|
|
153
|
+
seen.add(tool.name);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
export {
|
|
157
|
+
createMcpEngine
|
|
158
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Translates entity API errors into actionable messages for the LLM.
|
|
3
|
+
*
|
|
4
|
+
* Scope/permission failures arrive as HTTP_403, not PERMISSION_DENIED; CHECK
|
|
5
|
+
* violations (23514) are swallowed by the core as a bare 500 — overrides
|
|
6
|
+
* supply `errorHints` so those 500s still carry guidance.
|
|
7
|
+
*/
|
|
8
|
+
import type { EntityApiResult } from './types';
|
|
9
|
+
interface ErrorContext {
|
|
10
|
+
slug: string;
|
|
11
|
+
operation: string;
|
|
12
|
+
hints?: string[];
|
|
13
|
+
}
|
|
14
|
+
export declare function translateApiError(result: EntityApiResult, context: ErrorContext): string;
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/lib/mcp/errors.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAE9C,UAAU,YAAY;IACpB,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;CACjB;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe,EAAE,OAAO,EAAE,YAAY,GAAG,MAAM,CA4CxF"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
function translateApiError(result, context) {
|
|
2
|
+
var _a;
|
|
3
|
+
const { status, body } = result;
|
|
4
|
+
const code = body.code ?? `HTTP_${status}`;
|
|
5
|
+
const detail = body.error ? ` (API: ${body.error})` : "";
|
|
6
|
+
switch (code) {
|
|
7
|
+
case "AUTHENTICATION_FAILED":
|
|
8
|
+
return "API key inv\xE1lida, expirada o revocada. Gener\xE1 una nueva en Settings \u2192 API Keys y actualiz\xE1 la configuraci\xF3n del cliente MCP.";
|
|
9
|
+
case "TEAM_CONTEXT_REQUIRED":
|
|
10
|
+
return "Falta el contexto de equipo (team). Verific\xE1 que tu API key pertenezca a un usuario con un team activo, o envi\xE1 el header x-team-id.";
|
|
11
|
+
case "TEAM_ACCESS_DENIED":
|
|
12
|
+
return "El usuario de la API key no es miembro del team indicado. Verific\xE1 el x-team-id o us\xE1 el team por defecto.";
|
|
13
|
+
case "VALIDATION_ERROR":
|
|
14
|
+
return `Datos inv\xE1lidos para ${context.slug}.${formatDetails(body.details)} Correg\xED los campos y reintent\xE1.`;
|
|
15
|
+
case "INVALID_FIELD_VALUE":
|
|
16
|
+
return `Un campo tiene un valor no permitido${detail}. Revis\xE1 los valores del schema de la tool y reintent\xE1.`;
|
|
17
|
+
case "UNIQUE_CONSTRAINT_VIOLATION":
|
|
18
|
+
return `Ya existe un registro de ${context.slug} con ese valor \xFAnico${detail}. Buscalo con la tool de list y actualizalo en vez de crearlo.`;
|
|
19
|
+
case "RATE_LIMIT_EXCEEDED":
|
|
20
|
+
return "L\xEDmite de requests alcanzado. Esper\xE1 un minuto antes de reintentar; para cargas grandes us\xE1 las tools de batch si el entity las expone.";
|
|
21
|
+
default:
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
switch (status) {
|
|
25
|
+
case 401:
|
|
26
|
+
return "No autenticado: la API key no fue aceptada. Gener\xE1 una nueva en Settings \u2192 API Keys.";
|
|
27
|
+
case 403:
|
|
28
|
+
return `Sin permiso para ${context.operation} en ${context.slug}. Si es un problema de scope, tu API key necesita el scope ${context.slug}:${scopeFor(context.operation)}. Si es un problema de rol, tu rol en el team no permite esta acci\xF3n.`;
|
|
29
|
+
case 404:
|
|
30
|
+
return `No se encontr\xF3 el registro de ${context.slug}. El ID puede ser incorrecto o el registro fue eliminado \u2014 us\xE1 la tool de list para encontrar el ID correcto.`;
|
|
31
|
+
case 409:
|
|
32
|
+
return `No se puede completar: el registro de ${context.slug} tiene registros relacionados que dependen de \xE9l${detail}. Elimin\xE1 o reasign\xE1 los registros dependientes primero.`;
|
|
33
|
+
case 429:
|
|
34
|
+
return "L\xEDmite de requests alcanzado. Esper\xE1 un minuto antes de reintentar.";
|
|
35
|
+
case 500: {
|
|
36
|
+
const hints = ((_a = context.hints) == null ? void 0 : _a.length) ? ` Posibles causas conocidas: ${context.hints.join(" \xB7 ")}.` : "";
|
|
37
|
+
return `Error interno de la API al ejecutar ${context.operation} en ${context.slug}.${hints} Si persiste, reportalo.`;
|
|
38
|
+
}
|
|
39
|
+
default:
|
|
40
|
+
return `Error ${status} (${code}) al ejecutar ${context.operation} en ${context.slug}${detail}.`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function scopeFor(operation) {
|
|
44
|
+
if (operation === "list" || operation === "get") return "read";
|
|
45
|
+
if (operation === "delete") return "delete";
|
|
46
|
+
return "write";
|
|
47
|
+
}
|
|
48
|
+
function formatDetails(details) {
|
|
49
|
+
if (!details) return "";
|
|
50
|
+
if (Array.isArray(details)) {
|
|
51
|
+
const lines = details.map((issue) => {
|
|
52
|
+
if (issue && typeof issue === "object") {
|
|
53
|
+
const record = issue;
|
|
54
|
+
const path = Array.isArray(record.path) ? record.path.join(".") : String(record.path ?? "");
|
|
55
|
+
const message = String(record.message ?? JSON.stringify(record));
|
|
56
|
+
return path ? `${path}: ${message}` : message;
|
|
57
|
+
}
|
|
58
|
+
return String(issue);
|
|
59
|
+
}).slice(0, 8);
|
|
60
|
+
return ` Problemas: ${lines.join(" \xB7 ")}.`;
|
|
61
|
+
}
|
|
62
|
+
return ` Detalle: ${JSON.stringify(details).slice(0, 300)}.`;
|
|
63
|
+
}
|
|
64
|
+
export {
|
|
65
|
+
translateApiError
|
|
66
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process entity executor.
|
|
3
|
+
*
|
|
4
|
+
* Invokes the core generic handlers directly (exactly like
|
|
5
|
+
* app/api/v1/[entity]/route.ts does) with a synthesized NextRequest carrying
|
|
6
|
+
* the caller's Authorization and the resolved x-team-id. NO fetch fallback:
|
|
7
|
+
* if the in-process path fails it fails loudly; we never degrade to HTTP,
|
|
8
|
+
* which would send the credential to an env-derived URL.
|
|
9
|
+
*
|
|
10
|
+
* Team context is injected in ALL FIVE operations — the handler only reads
|
|
11
|
+
* the x-team-id header, and validates it on every operation.
|
|
12
|
+
*/
|
|
13
|
+
import type { EntityExecutor } from './types';
|
|
14
|
+
export declare const executeEntityOperation: EntityExecutor;
|
|
15
|
+
//# sourceMappingURL=executor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../../src/lib/mcp/executor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAUH,OAAO,KAAK,EAAmB,cAAc,EAA2C,MAAM,SAAS,CAAA;AAKvG,eAAO,MAAM,sBAAsB,EAAE,cA8BpC,CAAA"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { NextRequest } from "next/server";
|
|
2
|
+
import {
|
|
3
|
+
handleGenericList,
|
|
4
|
+
handleGenericCreate,
|
|
5
|
+
handleGenericRead,
|
|
6
|
+
handleGenericUpdate,
|
|
7
|
+
handleGenericDelete
|
|
8
|
+
} from "../api/entity/generic-handler.js";
|
|
9
|
+
const INTERNAL_ORIGIN = "http://mcp.internal";
|
|
10
|
+
const executeEntityOperation = async (args, ctx) => {
|
|
11
|
+
const request = buildRequest(args, ctx);
|
|
12
|
+
const params = Promise.resolve({ entity: args.slug, id: args.id ?? "" });
|
|
13
|
+
let response;
|
|
14
|
+
switch (args.operation) {
|
|
15
|
+
case "list":
|
|
16
|
+
response = await handleGenericList(request);
|
|
17
|
+
break;
|
|
18
|
+
case "create":
|
|
19
|
+
response = await handleGenericCreate(request);
|
|
20
|
+
break;
|
|
21
|
+
case "get":
|
|
22
|
+
response = await handleGenericRead(request, { params });
|
|
23
|
+
break;
|
|
24
|
+
case "update":
|
|
25
|
+
response = await handleGenericUpdate(request, { params });
|
|
26
|
+
break;
|
|
27
|
+
case "delete":
|
|
28
|
+
response = await handleGenericDelete(request, { params });
|
|
29
|
+
break;
|
|
30
|
+
default:
|
|
31
|
+
throw new Error(`Unsupported entity operation: ${String(args.operation)}`);
|
|
32
|
+
}
|
|
33
|
+
const body = await response.json();
|
|
34
|
+
return { status: response.status, body };
|
|
35
|
+
};
|
|
36
|
+
function buildRequest(args, ctx) {
|
|
37
|
+
const path = args.id ? `/api/v1/${args.slug}/${args.id}` : `/api/v1/${args.slug}`;
|
|
38
|
+
const url = new URL(path, INTERNAL_ORIGIN);
|
|
39
|
+
for (const [key, value] of Object.entries(args.query ?? {})) {
|
|
40
|
+
if (value !== void 0 && value !== "") url.searchParams.set(key, value);
|
|
41
|
+
}
|
|
42
|
+
const method = METHODS[args.operation];
|
|
43
|
+
const headers = {
|
|
44
|
+
authorization: ctx.authHeader,
|
|
45
|
+
"x-team-id": ctx.teamId
|
|
46
|
+
};
|
|
47
|
+
let body;
|
|
48
|
+
if (args.body !== void 0) {
|
|
49
|
+
body = JSON.stringify(args.body);
|
|
50
|
+
headers["content-type"] = "application/json";
|
|
51
|
+
}
|
|
52
|
+
return new NextRequest(url, { method, headers, body });
|
|
53
|
+
}
|
|
54
|
+
const METHODS = {
|
|
55
|
+
list: "GET",
|
|
56
|
+
get: "GET",
|
|
57
|
+
create: "POST",
|
|
58
|
+
update: "PATCH",
|
|
59
|
+
delete: "DELETE"
|
|
60
|
+
};
|
|
61
|
+
export {
|
|
62
|
+
executeEntityOperation
|
|
63
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { createMcpEngine, type McpEngine } from './engine';
|
|
2
|
+
export { executeEntityOperation } from './executor';
|
|
3
|
+
export { auditToolCall } from './audit';
|
|
4
|
+
export { translateApiError } from './errors';
|
|
5
|
+
export { buildEntitySchemas, type EntitySchemaBundle, type SchemaBuilderContext } from './schema-builder';
|
|
6
|
+
export { StatelessJsonRpcTransport } from './transport';
|
|
7
|
+
export { generateEntityTools, toolToken, listArgsToQuery, normalizeDateFields, errorResult, } from './tool-generator';
|
|
8
|
+
export { McpToolError, MUTATION_OPERATIONS, type AuditEntry, type EngineApi, type EntityApiResult, type EntityExecutor, type EntityPresetInfo, type ExecuteEntityArgs, type McpEngineOptions, type McpEntityOverride, type McpOperation, type McpShape, type McpToolAnnotations, type McpToolDefinition, type McpToolResult, type McpToolResultContent, type ToolExecutionContext, } from './types';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/mcp/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,KAAK,SAAS,EAAE,MAAM,UAAU,CAAA;AAC1D,OAAO,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAA;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AACvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAA;AAC5C,OAAO,EAAE,kBAAkB,EAAE,KAAK,kBAAkB,EAAE,KAAK,oBAAoB,EAAE,MAAM,kBAAkB,CAAA;AACzG,OAAO,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAA;AACvD,OAAO,EACL,mBAAmB,EACnB,SAAS,EACT,eAAe,EACf,mBAAmB,EACnB,WAAW,GACZ,MAAM,kBAAkB,CAAA;AACzB,OAAO,EACL,YAAY,EACZ,mBAAmB,EACnB,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,QAAQ,EACb,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,GAC1B,MAAM,SAAS,CAAA"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createMcpEngine } from "./engine.js";
|
|
2
|
+
import { executeEntityOperation } from "./executor.js";
|
|
3
|
+
import { auditToolCall } from "./audit.js";
|
|
4
|
+
import { translateApiError } from "./errors.js";
|
|
5
|
+
import { buildEntitySchemas } from "./schema-builder.js";
|
|
6
|
+
import { StatelessJsonRpcTransport } from "./transport.js";
|
|
7
|
+
import {
|
|
8
|
+
generateEntityTools,
|
|
9
|
+
toolToken,
|
|
10
|
+
listArgsToQuery,
|
|
11
|
+
normalizeDateFields,
|
|
12
|
+
errorResult
|
|
13
|
+
} from "./tool-generator.js";
|
|
14
|
+
import {
|
|
15
|
+
McpToolError,
|
|
16
|
+
MUTATION_OPERATIONS
|
|
17
|
+
} from "./types.js";
|
|
18
|
+
export {
|
|
19
|
+
MUTATION_OPERATIONS,
|
|
20
|
+
McpToolError,
|
|
21
|
+
StatelessJsonRpcTransport,
|
|
22
|
+
auditToolCall,
|
|
23
|
+
buildEntitySchemas,
|
|
24
|
+
createMcpEngine,
|
|
25
|
+
errorResult,
|
|
26
|
+
executeEntityOperation,
|
|
27
|
+
generateEntityTools,
|
|
28
|
+
listArgsToQuery,
|
|
29
|
+
normalizeDateFields,
|
|
30
|
+
toolToken,
|
|
31
|
+
translateApiError
|
|
32
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema builder — derives MCP tool input schemas from entity configs.
|
|
3
|
+
*
|
|
4
|
+
* DESIGN NOTE: the core's generateEntitySchemas output cannot back tools/list
|
|
5
|
+
* — its fields are built as z.union([..., z.undefined()]).transform(...),
|
|
6
|
+
* which zod 4 cannot convert to JSON Schema ("Undefined cannot be
|
|
7
|
+
* represented"). So this builder emits PRESENTATION schemas (JSON-Schema-
|
|
8
|
+
* friendly, LLM-documented) derived from the same field definitions. The core
|
|
9
|
+
* still validates: every call runs in-process through the generic handler,
|
|
10
|
+
* which validates with the real core schemas. The MCP layer enforces
|
|
11
|
+
* shape/enums/strictness; the core enforces semantics. This presentation
|
|
12
|
+
* layer must never be stricter than what the real API accepts.
|
|
13
|
+
*
|
|
14
|
+
* MCP-side hardening against the API's silent failures:
|
|
15
|
+
* - strict objects: unknown keys are rejected, not stripped
|
|
16
|
+
* - sortBy: enum of the entity's actually-sortable fields
|
|
17
|
+
* - filters: only real filterable fields; date/datetime EXCLUDED (the core
|
|
18
|
+
* applies `=` equality which never matches a timestamptz → silent [])
|
|
19
|
+
* - dateField/from/to validated as a trio (a lone `from` is silently ignored)
|
|
20
|
+
* - search only exposed when the entity has one of name|title|slug|content
|
|
21
|
+
* - `distinct` NEVER exposed (identifier interpolation risk in the core)
|
|
22
|
+
*/
|
|
23
|
+
import { z } from 'zod';
|
|
24
|
+
import type { EntityConfig } from '../entities/types';
|
|
25
|
+
import type { McpEntityOverride, McpShape } from './types';
|
|
26
|
+
export interface EntitySchemaBundle {
|
|
27
|
+
createShape: McpShape;
|
|
28
|
+
createStrict: z.ZodTypeAny;
|
|
29
|
+
updateShape: McpShape;
|
|
30
|
+
updateStrict: z.ZodTypeAny;
|
|
31
|
+
listShape: McpShape;
|
|
32
|
+
listStrict: z.ZodTypeAny;
|
|
33
|
+
getShape: McpShape;
|
|
34
|
+
getStrict: z.ZodTypeAny;
|
|
35
|
+
/** Field names of type date (DATE column) — outputs get normalized to YYYY-MM-DD. */
|
|
36
|
+
dateFields: string[];
|
|
37
|
+
/** Which field the core search param actually scans for this entity (or null). */
|
|
38
|
+
searchField: string | null;
|
|
39
|
+
}
|
|
40
|
+
export interface SchemaBuilderContext {
|
|
41
|
+
translateOption?: (slug: string, fieldName: string, value: string) => string | undefined;
|
|
42
|
+
override?: McpEntityOverride;
|
|
43
|
+
}
|
|
44
|
+
export declare function buildEntitySchemas(entityConfig: EntityConfig, context?: SchemaBuilderContext): EntitySchemaBundle;
|
|
45
|
+
//# sourceMappingURL=schema-builder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-builder.d.ts","sourceRoot":"","sources":["../../../src/lib/mcp/schema-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,YAAY,EAAe,MAAM,mBAAmB,CAAA;AAClE,OAAO,KAAK,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAwB1D,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,QAAQ,CAAA;IACrB,YAAY,EAAE,CAAC,CAAC,UAAU,CAAA;IAC1B,WAAW,EAAE,QAAQ,CAAA;IACrB,YAAY,EAAE,CAAC,CAAC,UAAU,CAAA;IAC1B,SAAS,EAAE,QAAQ,CAAA;IACnB,UAAU,EAAE,CAAC,CAAC,UAAU,CAAA;IACxB,QAAQ,EAAE,QAAQ,CAAA;IAClB,SAAS,EAAE,CAAC,CAAC,UAAU,CAAA;IACvB,qFAAqF;IACrF,UAAU,EAAE,MAAM,EAAE,CAAA;IACpB,kFAAkF;IAClF,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;CAC3B;AAED,MAAM,WAAW,oBAAoB;IACnC,eAAe,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;IACxF,QAAQ,CAAC,EAAE,iBAAiB,CAAA;CAC7B;AAED,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,YAAY,EAC1B,OAAO,GAAE,oBAAyB,GACjC,kBAAkB,CAwBpB"}
|