@nextsparkjs/core 0.1.0-beta.183 → 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/components/auth/forms/LoginForm.d.ts.map +1 -1
- package/dist/components/auth/forms/LoginForm.js +8 -5
- 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/messages/de/auth.json +2 -0
- package/dist/messages/de/index.d.ts +2 -0
- package/dist/messages/de/index.d.ts.map +1 -1
- package/dist/messages/en/auth.json +3 -1
- package/dist/messages/en/index.d.ts +2 -0
- package/dist/messages/en/index.d.ts.map +1 -1
- package/dist/messages/es/auth.json +2 -0
- package/dist/messages/es/index.d.ts +2 -0
- package/dist/messages/es/index.d.ts.map +1 -1
- package/dist/messages/fr/auth.json +2 -0
- package/dist/messages/fr/index.d.ts +2 -0
- package/dist/messages/fr/index.d.ts.map +1 -1
- package/dist/messages/it/auth.json +2 -0
- package/dist/messages/it/index.d.ts +2 -0
- package/dist/messages/it/index.d.ts.map +1 -1
- package/dist/messages/pt/auth.json +2 -0
- package/dist/messages/pt/index.d.ts +2 -0
- package/dist/messages/pt/index.d.ts.map +1 -1
- 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
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const SEARCHED_FIELDS = ["name", "title", "slug", "content"];
|
|
3
|
+
const INPUT_EXCLUDED_TYPES = /* @__PURE__ */ new Set(["media-library", "file", "image", "video", "audio"]);
|
|
4
|
+
const FILTER_EXCLUDED_TYPES = /* @__PURE__ */ new Set([
|
|
5
|
+
"datetime",
|
|
6
|
+
"json",
|
|
7
|
+
"media-library",
|
|
8
|
+
"textarea",
|
|
9
|
+
"richtext",
|
|
10
|
+
"markdown"
|
|
11
|
+
]);
|
|
12
|
+
function buildEntitySchemas(entityConfig, context = {}) {
|
|
13
|
+
const createShape = buildInputShape(entityConfig, "create", context);
|
|
14
|
+
const updateShape = buildInputShape(entityConfig, "update", context);
|
|
15
|
+
const listShape = buildListShape(entityConfig, context);
|
|
16
|
+
const getShape = {
|
|
17
|
+
id: z.string().min(1).describe(`ID del registro de ${entityConfig.names.singular} (uuid)`)
|
|
18
|
+
};
|
|
19
|
+
const dateFields = entityConfig.fields.filter((f) => f.type === "date").map((f) => f.name);
|
|
20
|
+
const searchField = SEARCHED_FIELDS.find((name) => entityConfig.fields.some((f) => f.name === name)) ?? null;
|
|
21
|
+
return {
|
|
22
|
+
createShape,
|
|
23
|
+
createStrict: z.object(createShape).strict(),
|
|
24
|
+
updateShape,
|
|
25
|
+
updateStrict: z.object(updateShape).strict(),
|
|
26
|
+
listShape,
|
|
27
|
+
listStrict: buildListStrict(listShape),
|
|
28
|
+
getShape,
|
|
29
|
+
getStrict: z.object(getShape).strict(),
|
|
30
|
+
dateFields,
|
|
31
|
+
searchField
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function presentationType(field) {
|
|
35
|
+
switch (field.type) {
|
|
36
|
+
case "select":
|
|
37
|
+
case "radio":
|
|
38
|
+
case "buttongroup":
|
|
39
|
+
case "combobox": {
|
|
40
|
+
const values = (field.options ?? []).map((opt) => String(opt.value));
|
|
41
|
+
return values.length ? z.enum(values) : z.string();
|
|
42
|
+
}
|
|
43
|
+
case "multiselect":
|
|
44
|
+
case "tags":
|
|
45
|
+
return z.array(z.string());
|
|
46
|
+
case "doublerange":
|
|
47
|
+
return z.array(z.number()).length(2);
|
|
48
|
+
case "number":
|
|
49
|
+
case "rating":
|
|
50
|
+
case "range":
|
|
51
|
+
case "currency": {
|
|
52
|
+
let type = z.number();
|
|
53
|
+
if (field.min !== void 0) type = type.min(field.min);
|
|
54
|
+
if (field.max !== void 0) type = type.max(field.max);
|
|
55
|
+
return type;
|
|
56
|
+
}
|
|
57
|
+
case "boolean":
|
|
58
|
+
return z.boolean();
|
|
59
|
+
case "date":
|
|
60
|
+
case "datetime":
|
|
61
|
+
case "email":
|
|
62
|
+
case "url":
|
|
63
|
+
case "phone":
|
|
64
|
+
case "timezone":
|
|
65
|
+
case "country":
|
|
66
|
+
return z.string().min(1);
|
|
67
|
+
case "relation":
|
|
68
|
+
case "relation-prop":
|
|
69
|
+
case "reference":
|
|
70
|
+
case "user":
|
|
71
|
+
return z.string().min(1);
|
|
72
|
+
case "relation-multi":
|
|
73
|
+
case "relation-prop-multi":
|
|
74
|
+
return z.array(z.string());
|
|
75
|
+
case "json":
|
|
76
|
+
case "address":
|
|
77
|
+
return z.record(z.string(), z.unknown());
|
|
78
|
+
case "text":
|
|
79
|
+
case "textarea":
|
|
80
|
+
case "markdown":
|
|
81
|
+
case "richtext":
|
|
82
|
+
case "code": {
|
|
83
|
+
let type = z.string();
|
|
84
|
+
if (field.maxLength) type = type.max(field.maxLength);
|
|
85
|
+
return type;
|
|
86
|
+
}
|
|
87
|
+
default:
|
|
88
|
+
console.warn(`[mcp] unmapped field type "${field.type}" (${field.name}) \u2014 using permissive schema`);
|
|
89
|
+
return z.unknown();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function buildInputShape(entityConfig, operation, context) {
|
|
93
|
+
var _a, _b;
|
|
94
|
+
const shape = {};
|
|
95
|
+
const relax = new Set(((_a = context.override) == null ? void 0 : _a.relaxRequired) ?? []);
|
|
96
|
+
for (const field of entityConfig.fields) {
|
|
97
|
+
if (INPUT_EXCLUDED_TYPES.has(field.type)) continue;
|
|
98
|
+
if ((_b = field.api) == null ? void 0 : _b.readOnly) continue;
|
|
99
|
+
let type = presentationType(field);
|
|
100
|
+
const required = operation === "create" && field.required && !relax.has(field.name);
|
|
101
|
+
if (!required) type = type.optional();
|
|
102
|
+
shape[field.name] = type.describe(describeField(entityConfig, field, field.name, context));
|
|
103
|
+
}
|
|
104
|
+
return shape;
|
|
105
|
+
}
|
|
106
|
+
function buildListShape(entityConfig, context) {
|
|
107
|
+
var _a;
|
|
108
|
+
const sortable = entityConfig.fields.filter((f) => {
|
|
109
|
+
var _a2;
|
|
110
|
+
return (_a2 = f.api) == null ? void 0 : _a2.sortable;
|
|
111
|
+
}).map((f) => f.name);
|
|
112
|
+
const sortableValues = [...sortable, "createdAt", "updatedAt"];
|
|
113
|
+
const dateFieldNames = entityConfig.fields.filter((f) => f.type === "date" || f.type === "datetime").map((f) => f.name);
|
|
114
|
+
const shape = {
|
|
115
|
+
page: z.number().int().min(1).optional().describe("P\xE1gina (1)"),
|
|
116
|
+
limit: z.number().int().min(1).max(100).optional().describe("Por p\xE1gina (10, m\xE1x 100)"),
|
|
117
|
+
sortBy: z.enum(sortableValues).optional().describe("Ordenar por"),
|
|
118
|
+
sortOrder: z.enum(["ASC", "DESC"]).optional().describe("Orden (DESC)")
|
|
119
|
+
};
|
|
120
|
+
const searchField = SEARCHED_FIELDS.find(
|
|
121
|
+
(name) => entityConfig.fields.some((f) => f.name === name)
|
|
122
|
+
);
|
|
123
|
+
if (searchField) {
|
|
124
|
+
shape.search = z.string().min(1).optional().describe(`Busca SOLO en "${searchField}"`);
|
|
125
|
+
}
|
|
126
|
+
if (dateFieldNames.length) {
|
|
127
|
+
shape.dateField = z.enum(dateFieldNames).optional().describe("Campo de fecha del rango (obligatorio con from/to)");
|
|
128
|
+
shape.from = z.string().optional().describe("Desde (YYYY-MM-DD)");
|
|
129
|
+
shape.to = z.string().optional().describe("Hasta (YYYY-MM-DD)");
|
|
130
|
+
}
|
|
131
|
+
const filterShape = {};
|
|
132
|
+
for (const field of entityConfig.fields) {
|
|
133
|
+
if (FILTER_EXCLUDED_TYPES.has(field.type)) continue;
|
|
134
|
+
if (INPUT_EXCLUDED_TYPES.has(field.type)) continue;
|
|
135
|
+
if (((_a = field.api) == null ? void 0 : _a.filterable) === false) continue;
|
|
136
|
+
filterShape[field.name] = presentationType(field).optional().describe(describeField(entityConfig, field, field.name, context, true));
|
|
137
|
+
}
|
|
138
|
+
if (Object.keys(filterShape).length) {
|
|
139
|
+
shape.filters = z.object(filterShape).strict().optional().describe("Filtros exactos. Fechas NO ac\xE1: us\xE1 dateField/from/to.");
|
|
140
|
+
}
|
|
141
|
+
const projectable = entityConfig.fields.map((f) => f.name);
|
|
142
|
+
if (projectable.length) {
|
|
143
|
+
shape.fields = z.array(z.enum(projectable)).min(1).optional().describe("Devolver solo estos campos (achica la respuesta)");
|
|
144
|
+
}
|
|
145
|
+
return shape;
|
|
146
|
+
}
|
|
147
|
+
function buildListStrict(listShape) {
|
|
148
|
+
return z.object(listShape).strict().superRefine((value, ctx) => {
|
|
149
|
+
const v = value;
|
|
150
|
+
if ((v.from || v.to) && !v.dateField) {
|
|
151
|
+
ctx.addIssue({
|
|
152
|
+
code: "custom",
|
|
153
|
+
path: ["dateField"],
|
|
154
|
+
message: "from/to requieren dateField \u2014 sin \xE9l la API los ignora en silencio"
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
function describeField(entityConfig, field, name, context, compact = false) {
|
|
160
|
+
var _a, _b, _c, _d, _e, _f;
|
|
161
|
+
const overrideDescription = (_c = (_b = (_a = context.override) == null ? void 0 : _a.describe) == null ? void 0 : _b.fields) == null ? void 0 : _c[name];
|
|
162
|
+
if (overrideDescription) return compact ? shorten(overrideDescription) : overrideDescription;
|
|
163
|
+
if (!field) return name;
|
|
164
|
+
const label = ((_d = field.display) == null ? void 0 : _d.label) || name;
|
|
165
|
+
if (compact) return label;
|
|
166
|
+
const parts = [label];
|
|
167
|
+
if (((_e = field.display) == null ? void 0 : _e.description) && field.display.description !== label) {
|
|
168
|
+
parts.push(field.display.description);
|
|
169
|
+
}
|
|
170
|
+
switch (field.type) {
|
|
171
|
+
case "select":
|
|
172
|
+
case "multiselect":
|
|
173
|
+
case "radio":
|
|
174
|
+
case "buttongroup":
|
|
175
|
+
case "combobox": {
|
|
176
|
+
const options = (field.options ?? []).map((opt) => {
|
|
177
|
+
var _a2;
|
|
178
|
+
const value = String(opt.value);
|
|
179
|
+
const translated = ((_a2 = context.translateOption) == null ? void 0 : _a2.call(context, entityConfig.slug, field.name, value)) ?? opt.label;
|
|
180
|
+
return translated && translated !== value ? `${value}=${translated}` : value;
|
|
181
|
+
}).join(", ");
|
|
182
|
+
if (options) parts.push(options);
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
case "relation":
|
|
186
|
+
if ((_f = field.relation) == null ? void 0 : _f.entity) {
|
|
187
|
+
parts.push(`uuid de ${field.relation.entity} (us\xE1 su tool de list)`);
|
|
188
|
+
}
|
|
189
|
+
break;
|
|
190
|
+
case "user":
|
|
191
|
+
parts.push("uuid de un miembro del team");
|
|
192
|
+
break;
|
|
193
|
+
case "date":
|
|
194
|
+
parts.push("YYYY-MM-DD");
|
|
195
|
+
break;
|
|
196
|
+
case "datetime":
|
|
197
|
+
parts.push("ISO 8601 con offset, ej. 2026-08-05T10:00:00-03:00");
|
|
198
|
+
break;
|
|
199
|
+
case "boolean":
|
|
200
|
+
parts.push("true/false (booleano, no string)");
|
|
201
|
+
break;
|
|
202
|
+
default:
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
return parts.join(". ");
|
|
206
|
+
}
|
|
207
|
+
function shorten(text) {
|
|
208
|
+
return text.length <= 60 ? text : `${text.slice(0, 57)}...`;
|
|
209
|
+
}
|
|
210
|
+
export {
|
|
211
|
+
buildEntitySchemas
|
|
212
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool generator — one entity config → its CRUD tool definitions.
|
|
3
|
+
*
|
|
4
|
+
* Names come from the entity config (names.singular/plural), never from naive
|
|
5
|
+
* singularization. Descriptions are fed primarily from the entity's API
|
|
6
|
+
* presets (summary → entity description, preset descriptions/payloads →
|
|
7
|
+
* few-shot examples). Delete tools require confirm:true and carry destructive
|
|
8
|
+
* warnings.
|
|
9
|
+
*
|
|
10
|
+
* No separate authorization gate is re-implemented here: every tool call
|
|
11
|
+
* round-trips in-process through the real generic entity handler (see
|
|
12
|
+
* executor.ts), which already runs `checkAuthPermission` + `hasRequiredScope`
|
|
13
|
+
* + ownership/field guards for API-key auth (fixed for #94/#95). A denied
|
|
14
|
+
* call surfaces as a normal API error, translated via errors.ts.
|
|
15
|
+
*/
|
|
16
|
+
import type { EntityConfig } from '../entities/types';
|
|
17
|
+
import { type EntityExecutor, type EntityPresetInfo, type McpEngineOptions, type McpEntityOverride, type McpOperation, type McpToolDefinition, type McpToolResult } from './types';
|
|
18
|
+
interface GeneratorDeps {
|
|
19
|
+
executor: EntityExecutor;
|
|
20
|
+
toolPrefix: string;
|
|
21
|
+
translateOption?: McpEngineOptions['translateOption'];
|
|
22
|
+
presets?: EntityPresetInfo;
|
|
23
|
+
}
|
|
24
|
+
export declare function generateEntityTools(entityConfig: EntityConfig, operations: McpOperation[], override: McpEntityOverride | undefined, deps: GeneratorDeps): McpToolDefinition[];
|
|
25
|
+
export declare function toolToken(name: string): string;
|
|
26
|
+
export declare function listArgsToQuery(args: Record<string, unknown>): Record<string, string>;
|
|
27
|
+
/** DATE columns come back serialized as timestamptz — normalize to YYYY-MM-DD. */
|
|
28
|
+
export declare function normalizeDateFields(row: unknown, dateFields: string[]): unknown;
|
|
29
|
+
export declare function errorResult(message: string): McpToolResult;
|
|
30
|
+
export {};
|
|
31
|
+
//# sourceMappingURL=tool-generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-generator.d.ts","sourceRoot":"","sources":["../../../src/lib/mcp/tool-generator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAGrD,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EAEjB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAEnB,MAAM,SAAS,CAAA;AAEhB,UAAU,aAAa;IACrB,QAAQ,EAAE,cAAc,CAAA;IACxB,UAAU,EAAE,MAAM,CAAA;IAClB,eAAe,CAAC,EAAE,gBAAgB,CAAC,iBAAiB,CAAC,CAAA;IACrD,OAAO,CAAC,EAAE,gBAAgB,CAAA;CAC3B;AAED,wBAAgB,mBAAmB,CACjC,YAAY,EAAE,YAAY,EAC1B,UAAU,EAAE,YAAY,EAAE,EAC1B,QAAQ,EAAE,iBAAiB,GAAG,SAAS,EACvC,IAAI,EAAE,aAAa,GAClB,iBAAiB,EAAE,CA6NrB;AAID,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAO9C;AA0BD,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAiBrF;AAED,kFAAkF;AAClF,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,GAAG,OAAO,CAU/E;AAkBD,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,CAE1D"}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { buildEntitySchemas } from "./schema-builder.js";
|
|
3
|
+
import { translateApiError } from "./errors.js";
|
|
4
|
+
import {
|
|
5
|
+
McpToolError
|
|
6
|
+
} from "./types.js";
|
|
7
|
+
function generateEntityTools(entityConfig, operations, override, deps) {
|
|
8
|
+
const schemas = buildEntitySchemas(entityConfig, {
|
|
9
|
+
translateOption: deps.translateOption,
|
|
10
|
+
override
|
|
11
|
+
});
|
|
12
|
+
const slug = entityConfig.slug;
|
|
13
|
+
const singular = toolToken(entityConfig.names.singular);
|
|
14
|
+
const plural = toolToken(slug);
|
|
15
|
+
const entityDescription = buildEntityDescription(entityConfig, override, deps.presets);
|
|
16
|
+
const tools = [];
|
|
17
|
+
const make = (operation, name, title, description, inputSchema, strictSchema, annotations, run) => {
|
|
18
|
+
var _a, _b;
|
|
19
|
+
return {
|
|
20
|
+
name,
|
|
21
|
+
title,
|
|
22
|
+
description: ((_b = (_a = override == null ? void 0 : override.describe) == null ? void 0 : _a.tools) == null ? void 0 : _b[operation]) ?? description,
|
|
23
|
+
inputSchema,
|
|
24
|
+
strictSchema,
|
|
25
|
+
annotations,
|
|
26
|
+
meta: { entitySlug: slug, operation },
|
|
27
|
+
handler: async (rawArgs, ctx) => {
|
|
28
|
+
const parsed = strictSchema.safeParse(rawArgs ?? {});
|
|
29
|
+
if (!parsed.success) {
|
|
30
|
+
return errorResult(
|
|
31
|
+
`Argumentos inv\xE1lidos para ${name}: ${formatZodIssues(parsed.error)}. Revis\xE1 el schema de la tool.`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
return await run(parsed.data, ctx);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (error instanceof McpToolError) return errorResult(error.message);
|
|
38
|
+
console.error(`[mcp] unexpected error in ${name}:`, error);
|
|
39
|
+
return errorResult(
|
|
40
|
+
`Error inesperado ejecutando ${name}: ${error instanceof Error ? error.message : String(error)}`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
const apiError = (result, operation) => errorResult(
|
|
47
|
+
translateApiError(result, { slug, operation, hints: override == null ? void 0 : override.errorHints })
|
|
48
|
+
);
|
|
49
|
+
if (operations.includes("list")) {
|
|
50
|
+
tools.push(
|
|
51
|
+
make(
|
|
52
|
+
"list",
|
|
53
|
+
`${deps.toolPrefix}_list_${plural}`,
|
|
54
|
+
`List ${entityConfig.names.plural}`,
|
|
55
|
+
`Lista ${entityConfig.names.plural} con paginaci\xF3n, orden y filtros. ${entityDescription}${presetExamples(deps.presets, "GET")}`,
|
|
56
|
+
schemas.listShape,
|
|
57
|
+
schemas.listStrict,
|
|
58
|
+
{ readOnlyHint: true },
|
|
59
|
+
async (args, ctx) => {
|
|
60
|
+
const query = listArgsToQuery(args);
|
|
61
|
+
const result = await deps.executor({ slug, operation: "list", query }, ctx);
|
|
62
|
+
if (!result.body.success) return apiError(result, "list");
|
|
63
|
+
const data = normalizeOutput(result.body.data);
|
|
64
|
+
const info = result.body.info ?? {};
|
|
65
|
+
const total = info.total ?? (Array.isArray(data) ? data.length : "?");
|
|
66
|
+
const page = info.page ?? 1;
|
|
67
|
+
const totalPages = info.totalPages ?? 1;
|
|
68
|
+
return {
|
|
69
|
+
content: [
|
|
70
|
+
{
|
|
71
|
+
type: "text",
|
|
72
|
+
text: `${String(total)} ${entityConfig.names.plural} (p\xE1gina ${String(page)}/${String(totalPages)})`
|
|
73
|
+
}
|
|
74
|
+
],
|
|
75
|
+
structuredContent: { data, info }
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (operations.includes("get")) {
|
|
82
|
+
tools.push(
|
|
83
|
+
make(
|
|
84
|
+
"get",
|
|
85
|
+
`${deps.toolPrefix}_get_${singular}`,
|
|
86
|
+
`Get ${entityConfig.names.singular}`,
|
|
87
|
+
`Obtiene un registro de ${entityConfig.names.singular} por ID. ${entityDescription}`,
|
|
88
|
+
schemas.getShape,
|
|
89
|
+
schemas.getStrict,
|
|
90
|
+
{ readOnlyHint: true },
|
|
91
|
+
async (args, ctx) => {
|
|
92
|
+
const result = await deps.executor(
|
|
93
|
+
{ slug, operation: "get", id: String(args.id) },
|
|
94
|
+
ctx
|
|
95
|
+
);
|
|
96
|
+
if (!result.body.success) return apiError(result, "get");
|
|
97
|
+
const data = normalizeOutput(result.body.data);
|
|
98
|
+
return {
|
|
99
|
+
content: [{ type: "text", text: `${entityConfig.names.singular} encontrado.` }],
|
|
100
|
+
structuredContent: { data }
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
if (operations.includes("create")) {
|
|
107
|
+
tools.push(
|
|
108
|
+
make(
|
|
109
|
+
"create",
|
|
110
|
+
`${deps.toolPrefix}_create_${singular}`,
|
|
111
|
+
`Create ${entityConfig.names.singular}`,
|
|
112
|
+
`Crea un registro de ${entityConfig.names.singular}. ${entityDescription}${presetExamples(deps.presets, "POST")}`,
|
|
113
|
+
schemas.createShape,
|
|
114
|
+
schemas.createStrict,
|
|
115
|
+
{},
|
|
116
|
+
async (args, ctx) => {
|
|
117
|
+
const input = (override == null ? void 0 : override.transformInput) ? await override.transformInput("create", args, ctx) : args;
|
|
118
|
+
const result = await deps.executor({ slug, operation: "create", body: input }, ctx);
|
|
119
|
+
if (!result.body.success) return apiError(result, "create");
|
|
120
|
+
const data = normalizeOutput(result.body.data);
|
|
121
|
+
const id = extractId(data);
|
|
122
|
+
return {
|
|
123
|
+
content: [
|
|
124
|
+
{ type: "text", text: `${entityConfig.names.singular} creado${id ? ` (id: ${id})` : ""}.` }
|
|
125
|
+
],
|
|
126
|
+
structuredContent: { data }
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
if (operations.includes("update")) {
|
|
133
|
+
const updateShape = {
|
|
134
|
+
id: z.string().min(1).describe(`ID del registro de ${entityConfig.names.singular} a modificar (uuid)`),
|
|
135
|
+
...schemas.updateShape
|
|
136
|
+
};
|
|
137
|
+
tools.push(
|
|
138
|
+
make(
|
|
139
|
+
"update",
|
|
140
|
+
`${deps.toolPrefix}_update_${singular}`,
|
|
141
|
+
`Update ${entityConfig.names.singular}`,
|
|
142
|
+
`Modifica campos de un registro de ${entityConfig.names.singular} (PATCH parcial: solo envi\xE1 los campos a cambiar). ${entityDescription}`,
|
|
143
|
+
updateShape,
|
|
144
|
+
z.object(updateShape).strict(),
|
|
145
|
+
{ idempotentHint: true },
|
|
146
|
+
async (args, ctx) => {
|
|
147
|
+
const { id, ...fields } = args;
|
|
148
|
+
if (!Object.keys(fields).length) {
|
|
149
|
+
return errorResult("No enviaste ning\xFAn campo a modificar.");
|
|
150
|
+
}
|
|
151
|
+
const input = (override == null ? void 0 : override.transformInput) ? await override.transformInput("update", fields, ctx) : fields;
|
|
152
|
+
const result = await deps.executor(
|
|
153
|
+
{ slug, operation: "update", id, body: input },
|
|
154
|
+
ctx
|
|
155
|
+
);
|
|
156
|
+
if (!result.body.success) return apiError(result, "update");
|
|
157
|
+
const data = normalizeOutput(result.body.data);
|
|
158
|
+
return {
|
|
159
|
+
content: [{ type: "text", text: `${entityConfig.names.singular} actualizado.` }],
|
|
160
|
+
structuredContent: { data }
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
if (operations.includes("delete")) {
|
|
167
|
+
const deleteShape = {
|
|
168
|
+
id: z.string().min(1).describe(`ID del registro de ${entityConfig.names.singular} a eliminar (uuid)`),
|
|
169
|
+
confirm: z.boolean().describe("true SOLO tras confirmaci\xF3n expl\xEDcita del usuario")
|
|
170
|
+
};
|
|
171
|
+
tools.push(
|
|
172
|
+
make(
|
|
173
|
+
"delete",
|
|
174
|
+
`${deps.toolPrefix}_delete_${singular}`,
|
|
175
|
+
`Delete ${entityConfig.names.singular}`,
|
|
176
|
+
`Elimina un ${entityConfig.names.singular}. DESTRUCTIVO: mostrale al usuario qu\xE9 vas a borrar y pedile confirmaci\xF3n antes.${deleteWarning(override)}`,
|
|
177
|
+
deleteShape,
|
|
178
|
+
z.object(deleteShape).strict(),
|
|
179
|
+
{ destructiveHint: true },
|
|
180
|
+
async (args, ctx) => {
|
|
181
|
+
if (args.confirm !== true) {
|
|
182
|
+
return errorResult(
|
|
183
|
+
"Eliminaci\xF3n no confirmada: pedile al usuario confirmaci\xF3n expl\xEDcita y reintent\xE1 con confirm: true."
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
const result = await deps.executor(
|
|
187
|
+
{ slug, operation: "delete", id: String(args.id) },
|
|
188
|
+
ctx
|
|
189
|
+
);
|
|
190
|
+
if (!result.body.success) return apiError(result, "delete");
|
|
191
|
+
return {
|
|
192
|
+
content: [{ type: "text", text: `${entityConfig.names.singular} eliminado (id: ${String(args.id)}).` }]
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
)
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
return tools;
|
|
199
|
+
}
|
|
200
|
+
function toolToken(name) {
|
|
201
|
+
return name.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
202
|
+
}
|
|
203
|
+
function buildEntityDescription(entityConfig, override, presets) {
|
|
204
|
+
var _a;
|
|
205
|
+
if ((_a = override == null ? void 0 : override.describe) == null ? void 0 : _a.entity) return override.describe.entity;
|
|
206
|
+
return (presets == null ? void 0 : presets.summary) ? `${presets.summary}.` : "";
|
|
207
|
+
}
|
|
208
|
+
function presetExamples(presets, method) {
|
|
209
|
+
if (!presets) return "";
|
|
210
|
+
const relevant = presets.presets.filter((p) => (p.method ?? "GET") === method);
|
|
211
|
+
const example = relevant.find((p) => method === "POST" ? p.payload : p.params);
|
|
212
|
+
if (!example) return "";
|
|
213
|
+
const body = method === "POST" ? example.payload : example.params;
|
|
214
|
+
const label = example.description || example.title || example.id;
|
|
215
|
+
return ` Ejemplo (${label}): ${JSON.stringify(body)}`;
|
|
216
|
+
}
|
|
217
|
+
function deleteWarning(override) {
|
|
218
|
+
var _a, _b;
|
|
219
|
+
const hint = (_b = (_a = override == null ? void 0 : override.describe) == null ? void 0 : _a.tools) == null ? void 0 : _b.delete;
|
|
220
|
+
return hint ? ` ${hint}` : "";
|
|
221
|
+
}
|
|
222
|
+
function listArgsToQuery(args) {
|
|
223
|
+
const query = {};
|
|
224
|
+
const { filters, fields, ...rest } = args;
|
|
225
|
+
for (const [key, value] of Object.entries(rest)) {
|
|
226
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
227
|
+
query[key] = String(value);
|
|
228
|
+
}
|
|
229
|
+
for (const [key, value] of Object.entries(filters ?? {})) {
|
|
230
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
231
|
+
query[key] = String(value);
|
|
232
|
+
}
|
|
233
|
+
if (fields == null ? void 0 : fields.length) query.fields = fields.join(",");
|
|
234
|
+
return query;
|
|
235
|
+
}
|
|
236
|
+
function normalizeDateFields(row, dateFields) {
|
|
237
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) return row;
|
|
238
|
+
const record = { ...row };
|
|
239
|
+
for (const field of dateFields) {
|
|
240
|
+
const value = record[field];
|
|
241
|
+
if (typeof value === "string" && value.length >= 10) {
|
|
242
|
+
record[field] = value.slice(0, 10);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return record;
|
|
246
|
+
}
|
|
247
|
+
function normalizeOutput(data) {
|
|
248
|
+
return data;
|
|
249
|
+
}
|
|
250
|
+
function extractId(data) {
|
|
251
|
+
if (data && typeof data === "object" && "id" in data) {
|
|
252
|
+
return String(data.id);
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
function errorResult(message) {
|
|
257
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
258
|
+
}
|
|
259
|
+
function formatZodIssues(error) {
|
|
260
|
+
return error.issues.slice(0, 6).map((issue) => {
|
|
261
|
+
const path = issue.path.join(".");
|
|
262
|
+
return path ? `${path}: ${issue.message}` : issue.message;
|
|
263
|
+
}).join(" \xB7 ");
|
|
264
|
+
}
|
|
265
|
+
export {
|
|
266
|
+
errorResult,
|
|
267
|
+
generateEntityTools,
|
|
268
|
+
listArgsToQuery,
|
|
269
|
+
normalizeDateFields,
|
|
270
|
+
toolToken
|
|
271
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stateless in-memory Transport for MCP over HTTP (App Router friendly).
|
|
3
|
+
*
|
|
4
|
+
* The official StreamableHTTPServerTransport requires Node's
|
|
5
|
+
* IncomingMessage/ServerResponse, which Next.js App Router does not expose.
|
|
6
|
+
* This transport bypasses HTTP entirely: the route handler parses the
|
|
7
|
+
* JSON-RPC body, feeds each message to an ephemeral McpServer through this
|
|
8
|
+
* transport, and returns the collected responses as plain JSON.
|
|
9
|
+
*
|
|
10
|
+
* Stateless by design: one server + one transport per request, no sessions,
|
|
11
|
+
* no SSE/server-push. Server-originated notifications are dropped.
|
|
12
|
+
*/
|
|
13
|
+
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
14
|
+
import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
|
|
15
|
+
export declare class StatelessJsonRpcTransport implements Transport {
|
|
16
|
+
onclose?: () => void;
|
|
17
|
+
onerror?: (error: Error) => void;
|
|
18
|
+
onmessage?: (message: JSONRPCMessage) => void;
|
|
19
|
+
private pending;
|
|
20
|
+
private closed;
|
|
21
|
+
start(): Promise<void>;
|
|
22
|
+
/** Called by the McpServer with responses (and notifications, which we drop). */
|
|
23
|
+
send(message: JSONRPCMessage): Promise<void>;
|
|
24
|
+
close(): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Feed one client message to the server. Resolves with the server's
|
|
27
|
+
* response for requests, or null for notifications (no response expected).
|
|
28
|
+
*/
|
|
29
|
+
handleMessage(message: JSONRPCMessage): Promise<JSONRPCMessage | null>;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../../src/lib/mcp/transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+CAA+C,CAAA;AAC9E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAA;AAMxE,qBAAa,yBAA0B,YAAW,SAAS;IACzD,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IAChC,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,cAAc,KAAK,IAAI,CAAA;IAE7C,OAAO,CAAC,OAAO,CAA8C;IAC7D,OAAO,CAAC,MAAM,CAAQ;IAEhB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5B,iFAAiF;IAC3E,IAAI,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAe5C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAM5B;;;OAGG;IACG,aAAa,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;CAsB7E"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const RESPONSE_TIMEOUT_MS = 3e4;
|
|
2
|
+
class StatelessJsonRpcTransport {
|
|
3
|
+
onclose;
|
|
4
|
+
onerror;
|
|
5
|
+
onmessage;
|
|
6
|
+
pending = /* @__PURE__ */ new Map();
|
|
7
|
+
closed = false;
|
|
8
|
+
async start() {
|
|
9
|
+
}
|
|
10
|
+
/** Called by the McpServer with responses (and notifications, which we drop). */
|
|
11
|
+
async send(message) {
|
|
12
|
+
if (this.closed) return;
|
|
13
|
+
const id = message.id;
|
|
14
|
+
if (id !== void 0 && id !== null && !("method" in message)) {
|
|
15
|
+
const resolve = this.pending.get(id);
|
|
16
|
+
if (resolve) {
|
|
17
|
+
this.pending.delete(id);
|
|
18
|
+
resolve(message);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async close() {
|
|
24
|
+
var _a;
|
|
25
|
+
this.closed = true;
|
|
26
|
+
this.pending.clear();
|
|
27
|
+
(_a = this.onclose) == null ? void 0 : _a.call(this);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Feed one client message to the server. Resolves with the server's
|
|
31
|
+
* response for requests, or null for notifications (no response expected).
|
|
32
|
+
*/
|
|
33
|
+
async handleMessage(message) {
|
|
34
|
+
if (!this.onmessage) {
|
|
35
|
+
throw new Error("Transport not connected to a server");
|
|
36
|
+
}
|
|
37
|
+
const isRequest = "method" in message && "id" in message && message.id !== null;
|
|
38
|
+
if (!isRequest) {
|
|
39
|
+
this.onmessage(message);
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const request = message;
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const timer = setTimeout(() => {
|
|
45
|
+
this.pending.delete(request.id);
|
|
46
|
+
reject(new Error(`MCP request ${String(request.id)} timed out`));
|
|
47
|
+
}, RESPONSE_TIMEOUT_MS);
|
|
48
|
+
this.pending.set(request.id, (response) => {
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
resolve(response);
|
|
51
|
+
});
|
|
52
|
+
this.onmessage(message);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export {
|
|
57
|
+
StatelessJsonRpcTransport
|
|
58
|
+
};
|