@opensaas/stack-core 0.37.0 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +191 -0
- package/CLAUDE.md +42 -3
- package/dist/access/access-filter.d.ts +70 -113
- package/dist/access/access-filter.d.ts.map +1 -1
- package/dist/access/access-filter.js +183 -210
- package/dist/access/access-filter.js.map +1 -1
- package/dist/access/access-filter.test.js +148 -188
- package/dist/access/access-filter.test.js.map +1 -1
- package/dist/access/declared-dependencies.d.ts +66 -26
- package/dist/access/declared-dependencies.d.ts.map +1 -1
- package/dist/access/declared-dependencies.js +62 -17
- package/dist/access/declared-dependencies.js.map +1 -1
- package/dist/access/declared-dependencies.test.d.ts +2 -0
- package/dist/access/declared-dependencies.test.d.ts.map +1 -0
- package/dist/access/declared-dependencies.test.js +226 -0
- package/dist/access/declared-dependencies.test.js.map +1 -0
- package/dist/access/depth-limits.d.ts +8 -7
- package/dist/access/depth-limits.d.ts.map +1 -1
- package/dist/access/depth-limits.js +8 -7
- package/dist/access/depth-limits.js.map +1 -1
- package/dist/access/engine.d.ts +0 -20
- package/dist/access/engine.d.ts.map +1 -1
- package/dist/access/engine.js +0 -25
- package/dist/access/engine.js.map +1 -1
- package/dist/access/errors.d.ts +61 -8
- package/dist/access/errors.d.ts.map +1 -1
- package/dist/access/errors.js +91 -12
- package/dist/access/errors.js.map +1 -1
- package/dist/access/field-access.d.ts +39 -1
- package/dist/access/field-access.d.ts.map +1 -1
- package/dist/access/field-access.js +108 -42
- package/dist/access/field-access.js.map +1 -1
- package/dist/access/field-access.test.js +229 -2
- package/dist/access/field-access.test.js.map +1 -1
- package/dist/access/field-transforms.d.ts +0 -27
- package/dist/access/field-transforms.d.ts.map +1 -1
- package/dist/access/field-visibility.d.ts +2 -5
- package/dist/access/field-visibility.d.ts.map +1 -1
- package/dist/access/field-visibility.js +89 -41
- package/dist/access/field-visibility.js.map +1 -1
- package/dist/access/index.d.ts +6 -3
- package/dist/access/index.d.ts.map +1 -1
- package/dist/access/index.js +12 -2
- package/dist/access/index.js.map +1 -1
- package/dist/access/query-validation.d.ts +121 -0
- package/dist/access/query-validation.d.ts.map +1 -0
- package/dist/access/query-validation.js +292 -0
- package/dist/access/query-validation.js.map +1 -0
- package/dist/access/relationship-count.d.ts +1 -1
- package/dist/access/relationship-count.d.ts.map +1 -1
- package/dist/access/relationship-count.js +2 -10
- package/dist/access/relationship-count.js.map +1 -1
- package/dist/access/relationship-label-filter.d.ts +23 -23
- package/dist/access/relationship-label-filter.d.ts.map +1 -1
- package/dist/access/relationship-label-filter.js +5 -76
- package/dist/access/relationship-label-filter.js.map +1 -1
- package/dist/access/relationship-label-filter.test.js +19 -56
- package/dist/access/relationship-label-filter.test.js.map +1 -1
- package/dist/access/transaction-registry.d.ts +45 -0
- package/dist/access/transaction-registry.d.ts.map +1 -0
- package/dist/access/transaction-registry.js +37 -0
- package/dist/access/transaction-registry.js.map +1 -0
- package/dist/access/types.d.ts +55 -59
- package/dist/access/types.d.ts.map +1 -1
- package/dist/config/index.d.ts +9 -7
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +10 -18
- package/dist/config/index.js.map +1 -1
- package/dist/config/label.d.ts +6 -13
- package/dist/config/label.d.ts.map +1 -1
- package/dist/config/label.js +6 -13
- package/dist/config/label.js.map +1 -1
- package/dist/config/nav-count.d.ts +11 -19
- package/dist/config/nav-count.d.ts.map +1 -1
- package/dist/config/nav-count.js +11 -19
- package/dist/config/nav-count.js.map +1 -1
- package/dist/config/plugin-engine.d.ts +0 -10
- package/dist/config/plugin-engine.d.ts.map +1 -1
- package/dist/config/plugin-engine.js +1 -37
- package/dist/config/plugin-engine.js.map +1 -1
- package/dist/config/types.d.ts +94 -109
- package/dist/config/types.d.ts.map +1 -1
- package/dist/context/apply-defaults.d.ts +8 -31
- package/dist/context/apply-defaults.d.ts.map +1 -1
- package/dist/context/apply-defaults.js +12 -41
- package/dist/context/apply-defaults.js.map +1 -1
- package/dist/context/hook-pipeline.d.ts +4 -29
- package/dist/context/hook-pipeline.d.ts.map +1 -1
- package/dist/context/hook-pipeline.js +8 -41
- package/dist/context/hook-pipeline.js.map +1 -1
- package/dist/context/index.d.ts +4 -15
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +223 -215
- package/dist/context/index.js.map +1 -1
- package/dist/context/nested-operations.d.ts +7 -26
- package/dist/context/nested-operations.d.ts.map +1 -1
- package/dist/context/nested-operations.js +73 -171
- package/dist/context/nested-operations.js.map +1 -1
- package/dist/context/transaction-boundary.d.ts +36 -47
- package/dist/context/transaction-boundary.d.ts.map +1 -1
- package/dist/context/transaction-boundary.js +92 -79
- package/dist/context/transaction-boundary.js.map +1 -1
- package/dist/context/write-pipeline.d.ts +25 -64
- package/dist/context/write-pipeline.d.ts.map +1 -1
- package/dist/context/write-pipeline.js +81 -111
- package/dist/context/write-pipeline.js.map +1 -1
- package/dist/fields/format-prisma-default.d.ts.map +1 -1
- package/dist/fields/format-prisma-default.js +4 -7
- package/dist/fields/format-prisma-default.js.map +1 -1
- package/dist/fields/index.d.ts +17 -2
- package/dist/fields/index.d.ts.map +1 -1
- package/dist/fields/index.js +136 -147
- package/dist/fields/index.js.map +1 -1
- package/dist/filter/collect.d.ts +26 -3
- package/dist/filter/collect.d.ts.map +1 -1
- package/dist/filter/collect.js +30 -12
- package/dist/filter/collect.js.map +1 -1
- package/dist/filter/filter.test.js +98 -15
- package/dist/filter/filter.test.js.map +1 -1
- package/dist/filter/index.d.ts +1 -0
- package/dist/filter/index.d.ts.map +1 -1
- package/dist/filter/index.js.map +1 -1
- package/dist/filter/map.d.ts.map +1 -1
- package/dist/filter/map.js +0 -5
- package/dist/filter/map.js.map +1 -1
- package/dist/filter/parse.d.ts.map +1 -1
- package/dist/filter/parse.js +2 -9
- package/dist/filter/parse.js.map +1 -1
- package/dist/filter/serialize.d.ts.map +1 -1
- package/dist/filter/serialize.js +2 -4
- package/dist/filter/serialize.js.map +1 -1
- package/dist/hooks/index.d.ts +4 -42
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +5 -59
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -4
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +1 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +4 -0
- package/dist/internal.js.map +1 -1
- package/dist/lib/case-utils.d.ts +0 -32
- package/dist/lib/case-utils.d.ts.map +1 -1
- package/dist/lib/case-utils.js +0 -32
- package/dist/lib/case-utils.js.map +1 -1
- package/dist/mcp/handler.d.ts +0 -4
- package/dist/mcp/handler.d.ts.map +1 -1
- package/dist/mcp/handler.js +12 -69
- package/dist/mcp/handler.js.map +1 -1
- package/dist/query/index.d.ts +55 -227
- package/dist/query/index.d.ts.map +1 -1
- package/dist/query/index.js +41 -117
- package/dist/query/index.js.map +1 -1
- package/dist/query/relationship-options.d.ts +1 -1
- package/dist/query/relationship-options.js +1 -1
- package/dist/utils/password.d.ts +0 -68
- package/dist/utils/password.d.ts.map +1 -1
- package/dist/utils/password.js +7 -76
- package/dist/utils/password.js.map +1 -1
- package/dist/validation/field-config.d.ts.map +1 -1
- package/dist/validation/field-config.js +0 -6
- package/dist/validation/field-config.js.map +1 -1
- package/dist/validation/schema.d.ts +0 -7
- package/dist/validation/schema.d.ts.map +1 -1
- package/dist/validation/schema.js +0 -10
- package/dist/validation/schema.js.map +1 -1
- package/package.json +1 -1
- package/src/access/access-filter.test.ts +205 -275
- package/src/access/access-filter.ts +236 -272
- package/src/access/declared-dependencies.test.ts +277 -0
- package/src/access/declared-dependencies.ts +117 -37
- package/src/access/depth-limits.ts +8 -7
- package/src/access/engine.ts +0 -25
- package/src/access/errors.ts +97 -12
- package/src/access/field-access.test.ts +312 -2
- package/src/access/field-access.ts +118 -42
- package/src/access/field-transforms.ts +8 -39
- package/src/access/field-visibility.ts +99 -45
- package/src/access/index.ts +17 -5
- package/src/access/query-validation.ts +359 -0
- package/src/access/relationship-count.ts +3 -11
- package/src/access/relationship-label-filter.test.ts +19 -68
- package/src/access/relationship-label-filter.ts +27 -104
- package/src/access/transaction-registry.ts +58 -0
- package/src/access/types.ts +83 -86
- package/src/config/index.ts +10 -22
- package/src/config/label.ts +6 -13
- package/src/config/nav-count.ts +11 -19
- package/src/config/plugin-engine.ts +1 -37
- package/src/config/types.ts +95 -110
- package/src/context/apply-defaults.ts +12 -44
- package/src/context/hook-pipeline.ts +12 -67
- package/src/context/index.ts +311 -276
- package/src/context/nested-operations.ts +94 -226
- package/src/context/transaction-boundary.ts +113 -109
- package/src/context/write-pipeline.ts +99 -155
- package/src/fields/format-prisma-default.ts +4 -7
- package/src/fields/index.ts +155 -147
- package/src/filter/collect.ts +48 -12
- package/src/filter/filter.test.ts +153 -15
- package/src/filter/index.ts +1 -0
- package/src/filter/map.ts +0 -5
- package/src/filter/parse.ts +2 -9
- package/src/filter/serialize.ts +2 -4
- package/src/hooks/index.ts +5 -59
- package/src/index.ts +23 -4
- package/src/internal.ts +5 -0
- package/src/lib/case-utils.ts +0 -32
- package/src/mcp/handler.ts +13 -71
- package/src/query/index.ts +82 -235
- package/src/query/relationship-options.ts +1 -1
- package/src/utils/password.ts +7 -76
- package/src/validation/field-config.ts +0 -6
- package/src/validation/schema.ts +0 -10
- package/tests/access-relationships.test.ts +18 -16
- package/tests/computed-field-selective-evaluation.test.ts +418 -0
- package/tests/context.test.ts +727 -1
- package/tests/field-types.test.ts +222 -0
- package/tests/mcp-handler.test.ts +34 -0
- package/tests/needs-declared-dependencies.test.ts +7 -4
- package/tests/resolve-chain.test.ts +11 -11
- package/tests/transaction-boundary-outermost.test.ts +530 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/index.ts
CHANGED
|
@@ -75,6 +75,22 @@ export { AccessScopeDepthExceededError } from './access/index.js'
|
|
|
75
75
|
// from `ValidationError` for the same reason as `AccessScopeDepthExceededError`.
|
|
76
76
|
export { ResolveOutputCycleError } from './access/index.js'
|
|
77
77
|
|
|
78
|
+
// Thrown by `checkFieldAccess` when a field-level access control function
|
|
79
|
+
// returns anything other than a strict boolean (see ADR-0001 and ADR-0030).
|
|
80
|
+
// Distinct from `ValidationError` for the same reason as
|
|
81
|
+
// `AccessScopeDepthExceededError` — this is the engine refusing to interpret
|
|
82
|
+
// a result it cannot treat as an allow/deny decision, not a user-input
|
|
83
|
+
// validation failure.
|
|
84
|
+
export { InvalidFieldAccessResultError } from './access/index.js'
|
|
85
|
+
|
|
86
|
+
// Thrown by a read when a caller-supplied `where` filters on a relation whose
|
|
87
|
+
// related list denies operation-level `query` access outright (see #916 and
|
|
88
|
+
// ADR-0022). Distinct from `ValidationError` for the same reason as
|
|
89
|
+
// `AccessScopeDepthExceededError` — this is the engine declining to return a
|
|
90
|
+
// silently-narrowed match on a relation it cannot scope, not a user-input
|
|
91
|
+
// validation failure.
|
|
92
|
+
export { RelationFilterAccessDeniedError } from './access/index.js'
|
|
93
|
+
|
|
78
94
|
// Field self-containment validation — checks each field implements the
|
|
79
95
|
// generation contract (getPrismaType / getTypeScriptType / getZodSchema, or
|
|
80
96
|
// getPrismaRelation for relationships) so a misimplemented field fails early
|
|
@@ -127,6 +143,7 @@ export type {
|
|
|
127
143
|
FilterValueSource,
|
|
128
144
|
FilterFieldSuggestion,
|
|
129
145
|
RelationshipCountFilterMarker,
|
|
146
|
+
FilterAccessArgs,
|
|
130
147
|
} from './filter/index.js'
|
|
131
148
|
|
|
132
149
|
// Access-scoped to-many relationship counts for the admin list view (#732):
|
|
@@ -139,10 +156,12 @@ export {
|
|
|
139
156
|
isToManyRelationshipField,
|
|
140
157
|
} from './access/relationship-count.js'
|
|
141
158
|
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
159
|
+
// To-one relationship label filter helpers for the admin list view (#749).
|
|
160
|
+
// `resolveRelationshipLabelFilters` is now a pass-through: the engine itself
|
|
161
|
+
// scopes every relation filter in `where` (`buildAccessScopedWhere`, #916),
|
|
162
|
+
// including the `{ is: {...} } }` shape a label filter produces, so this no
|
|
163
|
+
// longer needs its own access fold. Kept exported, unchanged in shape, for
|
|
164
|
+
// API compatibility — see `relationship-label-filter.ts`'s doc comment.
|
|
146
165
|
export {
|
|
147
166
|
resolveRelationshipLabelFilters,
|
|
148
167
|
isToOneRelationshipField,
|
package/src/internal.ts
CHANGED
|
@@ -41,6 +41,11 @@ export { validateWithZod, generateZodSchema } from './validation/schema.js'
|
|
|
41
41
|
// This is the single field-access evaluator — the UI must not re-implement it.
|
|
42
42
|
export { checkFieldAccess } from './access/index.js'
|
|
43
43
|
|
|
44
|
+
// Predicate-time field-read evaluator (#915), reused by @opensaas/stack-ui to
|
|
45
|
+
// keep the admin list view's sort validation in lockstep with the engine: a
|
|
46
|
+
// field the session cannot read cannot seed an `orderBy` either.
|
|
47
|
+
export { isFieldReadableForPredicate } from './access/index.js'
|
|
48
|
+
|
|
44
49
|
// Config-shape sub-types consumed by sibling packages (not part of the consumer surface)
|
|
45
50
|
export type {
|
|
46
51
|
DatabaseConfig,
|
package/src/lib/case-utils.ts
CHANGED
|
@@ -8,31 +8,16 @@
|
|
|
8
8
|
* - URLs: kebab-case (e.g., "auth-user", "blog-post")
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
* Convert PascalCase to camelCase
|
|
13
|
-
* AuthUser -> authUser
|
|
14
|
-
* BlogPost -> blogPost
|
|
15
|
-
*/
|
|
16
11
|
export function pascalToCamel(str: string): string {
|
|
17
12
|
return str.charAt(0).toLowerCase() + str.slice(1)
|
|
18
13
|
}
|
|
19
14
|
|
|
20
|
-
/**
|
|
21
|
-
* Convert PascalCase to kebab-case
|
|
22
|
-
* AuthUser -> auth-user
|
|
23
|
-
* BlogPost -> blog-post
|
|
24
|
-
*/
|
|
25
15
|
export function pascalToKebab(str: string): string {
|
|
26
16
|
return str.replace(/([A-Z])/g, (match, p1, offset) => {
|
|
27
17
|
return offset > 0 ? `-${p1.toLowerCase()}` : p1.toLowerCase()
|
|
28
18
|
})
|
|
29
19
|
}
|
|
30
20
|
|
|
31
|
-
/**
|
|
32
|
-
* Convert kebab-case to PascalCase
|
|
33
|
-
* auth-user -> AuthUser
|
|
34
|
-
* blog-post -> BlogPost
|
|
35
|
-
*/
|
|
36
21
|
export function kebabToPascal(str: string): string {
|
|
37
22
|
return str
|
|
38
23
|
.split('-')
|
|
@@ -40,35 +25,18 @@ export function kebabToPascal(str: string): string {
|
|
|
40
25
|
.join('')
|
|
41
26
|
}
|
|
42
27
|
|
|
43
|
-
/**
|
|
44
|
-
* Convert kebab-case to camelCase
|
|
45
|
-
* auth-user -> authUser
|
|
46
|
-
* blog-post -> blogPost
|
|
47
|
-
*/
|
|
48
28
|
export function kebabToCamel(str: string): string {
|
|
49
29
|
return str.replace(/-([a-z])/g, (match, p1) => p1.toUpperCase())
|
|
50
30
|
}
|
|
51
31
|
|
|
52
|
-
/**
|
|
53
|
-
* Get the database key for a list (camelCase)
|
|
54
|
-
* Used for accessing context.db and prisma client
|
|
55
|
-
*/
|
|
56
32
|
export function getDbKey(listKey: string): string {
|
|
57
33
|
return pascalToCamel(listKey)
|
|
58
34
|
}
|
|
59
35
|
|
|
60
|
-
/**
|
|
61
|
-
* Get the URL segment for a list (kebab-case)
|
|
62
|
-
* Used for constructing admin URLs
|
|
63
|
-
*/
|
|
64
36
|
export function getUrlKey(listKey: string): string {
|
|
65
37
|
return pascalToKebab(listKey)
|
|
66
38
|
}
|
|
67
39
|
|
|
68
|
-
/**
|
|
69
|
-
* Get the list key from a URL segment (PascalCase)
|
|
70
|
-
* Used for parsing admin URLs
|
|
71
|
-
*/
|
|
72
40
|
export function getListKeyFromUrl(urlSegment: string): string {
|
|
73
41
|
return kebabToPascal(urlSegment)
|
|
74
42
|
}
|
package/src/mcp/handler.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Runtime MCP route handler
|
|
3
|
-
* Creates MCP API handlers from OpenSaaS config at runtime
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
1
|
import * as z from 'zod'
|
|
7
2
|
import type { OpenSaasConfig, FieldConfig, McpCustomTool } from '../config/types.js'
|
|
8
3
|
import type { AccessContext } from '../access/types.js'
|
|
@@ -16,12 +11,7 @@ import type { McpSession, McpSessionProvider } from './types.js'
|
|
|
16
11
|
*/
|
|
17
12
|
type ContextSession = { userId: string; [key: string]: unknown }
|
|
18
13
|
|
|
19
|
-
/**
|
|
20
|
-
* Convert an MCP session into a context session.
|
|
21
|
-
* Transport-level fields (accessToken, expiresAt, scopes) are stripped;
|
|
22
|
-
* everything else — userId plus any custom fields the session provider
|
|
23
|
-
* attached (email, role, ...) — flows through to access control.
|
|
24
|
-
*/
|
|
14
|
+
/** Strips transport-level fields; userId and any custom session fields flow through to access control. */
|
|
25
15
|
function toContextSession(session: McpSession): ContextSession {
|
|
26
16
|
const { accessToken: _accessToken, expiresAt: _expiresAt, scopes: _scopes, ...rest } = session
|
|
27
17
|
return rest as ContextSession
|
|
@@ -35,20 +25,12 @@ function getPluginMcpTools(config: OpenSaasConfig): McpCustomTool[] {
|
|
|
35
25
|
return (config._pluginData?.__mcpTools as McpCustomTool[] | undefined) ?? []
|
|
36
26
|
}
|
|
37
27
|
|
|
38
|
-
/**
|
|
39
|
-
* Whether a custom tool's inputSchema is a Zod schema (as opposed to a plain
|
|
40
|
-
* JSON Schema object). Duck-typed so it works across zod module instances.
|
|
41
|
-
*/
|
|
42
28
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- duck-typing across zod instances
|
|
43
29
|
function isZodSchema(schema: any): schema is z.ZodType {
|
|
44
30
|
return !!schema && typeof schema.safeParse === 'function'
|
|
45
31
|
}
|
|
46
32
|
|
|
47
|
-
/**
|
|
48
|
-
* Normalize a custom tool's inputSchema for the tools/list response.
|
|
49
|
-
* Zod schemas are converted to JSON Schema (the MCP wire format); plain
|
|
50
|
-
* objects are passed through as-is.
|
|
51
|
-
*/
|
|
33
|
+
/** Zod schemas are converted to JSON Schema (the MCP wire format); plain objects pass through as-is. */
|
|
52
34
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- inputSchema is user-supplied
|
|
53
35
|
function toolInputSchemaToJson(inputSchema: any): McpTool['inputSchema'] {
|
|
54
36
|
if (isZodSchema(inputSchema)) {
|
|
@@ -96,7 +78,6 @@ export function createMcpHandlers(options: {
|
|
|
96
78
|
} {
|
|
97
79
|
const { config, getSession, getContext } = options
|
|
98
80
|
|
|
99
|
-
// Validate MCP is enabled
|
|
100
81
|
if (!config.mcp?.enabled) {
|
|
101
82
|
const notEnabledHandler = async () =>
|
|
102
83
|
new Response(JSON.stringify({ error: 'MCP not enabled' }), {
|
|
@@ -108,11 +89,7 @@ export function createMcpHandlers(options: {
|
|
|
108
89
|
|
|
109
90
|
const basePath = config.mcp.basePath || '/api/mcp'
|
|
110
91
|
|
|
111
|
-
/**
|
|
112
|
-
* Main MCP request handler
|
|
113
|
-
*/
|
|
114
92
|
const handler = async (req: Request): Promise<Response> => {
|
|
115
|
-
// Authenticate using provided session provider
|
|
116
93
|
const session = await getSession(req.headers)
|
|
117
94
|
if (!session) {
|
|
118
95
|
return new Response(null, {
|
|
@@ -132,23 +109,19 @@ export function createMcpHandlers(options: {
|
|
|
132
109
|
params?: any
|
|
133
110
|
}
|
|
134
111
|
|
|
135
|
-
// Handle initialize
|
|
136
112
|
if (body.method === 'initialize') {
|
|
137
113
|
return handleInitialize(body.params, body.id)
|
|
138
114
|
}
|
|
139
115
|
|
|
140
|
-
// Handle notifications/initialized (sent by client after initialize response)
|
|
141
116
|
if (body.method === 'notifications/initialized') {
|
|
142
117
|
// Notifications don't require a response in JSON-RPC 2.0
|
|
143
118
|
return new Response(null, { status: 204 })
|
|
144
119
|
}
|
|
145
120
|
|
|
146
|
-
// Handle tools/list
|
|
147
121
|
if (body.method === 'tools/list') {
|
|
148
122
|
return handleToolsList(config, body.id)
|
|
149
123
|
}
|
|
150
124
|
|
|
151
|
-
// Handle tools/call
|
|
152
125
|
if (body.method === 'tools/call') {
|
|
153
126
|
return await handleToolsCall(body.params, session, config, getContext, body.id)
|
|
154
127
|
}
|
|
@@ -198,9 +171,6 @@ type McpTool = {
|
|
|
198
171
|
}
|
|
199
172
|
}
|
|
200
173
|
|
|
201
|
-
/**
|
|
202
|
-
* Handle initialize request - respond with server capabilities
|
|
203
|
-
*/
|
|
204
174
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Initialize params are from the client
|
|
205
175
|
function handleInitialize(_params?: any, id?: number | string): Response {
|
|
206
176
|
return new Response(
|
|
@@ -224,9 +194,6 @@ function handleInitialize(_params?: any, id?: number | string): Response {
|
|
|
224
194
|
)
|
|
225
195
|
}
|
|
226
196
|
|
|
227
|
-
/**
|
|
228
|
-
* Convert field config to JSON schema property
|
|
229
|
-
*/
|
|
230
197
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Field configs have varying structures
|
|
231
198
|
function fieldToJsonSchema(fieldName: string, fieldConfig: any): Record<string, unknown> {
|
|
232
199
|
const baseSchema: Record<string, unknown> = {}
|
|
@@ -261,7 +228,6 @@ function fieldToJsonSchema(fieldName: string, fieldConfig: any): Record<string,
|
|
|
261
228
|
}
|
|
262
229
|
break
|
|
263
230
|
case 'relationship':
|
|
264
|
-
// For relationships, expect an ID or connect object
|
|
265
231
|
baseSchema.type = 'object'
|
|
266
232
|
baseSchema.properties = {
|
|
267
233
|
connect: {
|
|
@@ -273,17 +239,12 @@ function fieldToJsonSchema(fieldName: string, fieldConfig: any): Record<string,
|
|
|
273
239
|
}
|
|
274
240
|
break
|
|
275
241
|
default:
|
|
276
|
-
// For custom field types, default to string
|
|
277
242
|
baseSchema.type = 'string'
|
|
278
243
|
}
|
|
279
244
|
|
|
280
245
|
return baseSchema
|
|
281
246
|
}
|
|
282
247
|
|
|
283
|
-
/**
|
|
284
|
-
* Generate field schemas for create/update operations
|
|
285
|
-
*/
|
|
286
|
-
|
|
287
248
|
function generateFieldSchemas(
|
|
288
249
|
fields: Record<string, FieldConfig>,
|
|
289
250
|
operation: 'create' | 'update',
|
|
@@ -295,12 +256,10 @@ function generateFieldSchemas(
|
|
|
295
256
|
const required: string[] = []
|
|
296
257
|
|
|
297
258
|
for (const [fieldName, fieldConfig] of Object.entries(fields)) {
|
|
298
|
-
// Skip system fields
|
|
299
259
|
if (['id', 'createdAt', 'updatedAt'].includes(fieldName)) continue
|
|
300
260
|
|
|
301
261
|
properties[fieldName] = fieldToJsonSchema(fieldName, fieldConfig)
|
|
302
262
|
|
|
303
|
-
// Add to required array if field is required for this operation
|
|
304
263
|
if (
|
|
305
264
|
operation === 'create' &&
|
|
306
265
|
'validation' in fieldConfig &&
|
|
@@ -314,15 +273,10 @@ function generateFieldSchemas(
|
|
|
314
273
|
return { properties, required }
|
|
315
274
|
}
|
|
316
275
|
|
|
317
|
-
/**
|
|
318
|
-
* Handle tools/list request - list all available tools
|
|
319
|
-
*/
|
|
320
276
|
function handleToolsList(config: OpenSaasConfig, id?: number | string): Response {
|
|
321
277
|
const tools: McpTool[] = []
|
|
322
278
|
|
|
323
|
-
// Generate CRUD tools for each list
|
|
324
279
|
for (const [listKey, listConfig] of Object.entries(config.lists)) {
|
|
325
|
-
// Check if MCP is enabled for this list
|
|
326
280
|
if (listConfig.mcp?.enabled === false) continue
|
|
327
281
|
|
|
328
282
|
const dbKey = getDbKey(listKey)
|
|
@@ -340,7 +294,6 @@ function handleToolsList(config: OpenSaasConfig, id?: number | string): Response
|
|
|
340
294
|
delete: listConfig.mcp?.tools?.delete ?? defaultTools.delete ?? true,
|
|
341
295
|
}
|
|
342
296
|
|
|
343
|
-
// Read tool
|
|
344
297
|
if (enabledTools.read) {
|
|
345
298
|
tools.push({
|
|
346
299
|
name: `list_${dbKey}_query`,
|
|
@@ -357,7 +310,6 @@ function handleToolsList(config: OpenSaasConfig, id?: number | string): Response
|
|
|
357
310
|
})
|
|
358
311
|
}
|
|
359
312
|
|
|
360
|
-
// Create tool
|
|
361
313
|
if (enabledTools.create) {
|
|
362
314
|
const fieldSchemas = generateFieldSchemas(listConfig.fields, 'create')
|
|
363
315
|
tools.push({
|
|
@@ -378,7 +330,6 @@ function handleToolsList(config: OpenSaasConfig, id?: number | string): Response
|
|
|
378
330
|
})
|
|
379
331
|
}
|
|
380
332
|
|
|
381
|
-
// Update tool
|
|
382
333
|
if (enabledTools.update) {
|
|
383
334
|
const fieldSchemas = generateFieldSchemas(listConfig.fields, 'update')
|
|
384
335
|
tools.push({
|
|
@@ -406,7 +357,6 @@ function handleToolsList(config: OpenSaasConfig, id?: number | string): Response
|
|
|
406
357
|
})
|
|
407
358
|
}
|
|
408
359
|
|
|
409
|
-
// Delete tool
|
|
410
360
|
if (enabledTools.delete) {
|
|
411
361
|
tools.push({
|
|
412
362
|
name: `list_${dbKey}_delete`,
|
|
@@ -428,7 +378,6 @@ function handleToolsList(config: OpenSaasConfig, id?: number | string): Response
|
|
|
428
378
|
})
|
|
429
379
|
}
|
|
430
380
|
|
|
431
|
-
// Custom tools
|
|
432
381
|
if (listConfig.mcp?.customTools) {
|
|
433
382
|
for (const customTool of listConfig.mcp.customTools) {
|
|
434
383
|
tools.push({
|
|
@@ -461,9 +410,6 @@ function handleToolsList(config: OpenSaasConfig, id?: number | string): Response
|
|
|
461
410
|
)
|
|
462
411
|
}
|
|
463
412
|
|
|
464
|
-
/**
|
|
465
|
-
* Handle tools/call request - execute a tool
|
|
466
|
-
*/
|
|
467
413
|
async function handleToolsCall(
|
|
468
414
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- MCP tool params vary by tool
|
|
469
415
|
params: any,
|
|
@@ -489,7 +435,6 @@ async function handleToolsCall(
|
|
|
489
435
|
)
|
|
490
436
|
}
|
|
491
437
|
|
|
492
|
-
// Parse tool name: list_{dbKey}_{operation}
|
|
493
438
|
const match = toolName.match(/^list_([a-z][a-zA-Z0-9]*)_(query|create|update|delete)$/)
|
|
494
439
|
|
|
495
440
|
if (match) {
|
|
@@ -497,13 +442,9 @@ async function handleToolsCall(
|
|
|
497
442
|
return await handleCrudTool(dbKey, operation, toolArgs, session, config, getContext, id)
|
|
498
443
|
}
|
|
499
444
|
|
|
500
|
-
// Handle custom tools
|
|
501
445
|
return await handleCustomTool(toolName, toolArgs, session, config, getContext, id)
|
|
502
446
|
}
|
|
503
447
|
|
|
504
|
-
/**
|
|
505
|
-
* Handle CRUD tool execution
|
|
506
|
-
*/
|
|
507
448
|
async function handleCrudTool(
|
|
508
449
|
dbKey: string,
|
|
509
450
|
operation: string,
|
|
@@ -514,7 +455,6 @@ async function handleCrudTool(
|
|
|
514
455
|
getContext: (session?: ContextSession) => Promise<AccessContext>,
|
|
515
456
|
id?: number | string,
|
|
516
457
|
): Promise<Response> {
|
|
517
|
-
// Create context with the user session (custom session fields pass through)
|
|
518
458
|
const context = await getContext(toContextSession(session))
|
|
519
459
|
|
|
520
460
|
try {
|
|
@@ -585,9 +525,6 @@ async function handleCrudTool(
|
|
|
585
525
|
}
|
|
586
526
|
}
|
|
587
527
|
|
|
588
|
-
/**
|
|
589
|
-
* Handle custom tool execution
|
|
590
|
-
*/
|
|
591
528
|
async function handleCustomTool(
|
|
592
529
|
toolName: string,
|
|
593
530
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Custom tool arguments are user-defined
|
|
@@ -609,7 +546,6 @@ async function handleCustomTool(
|
|
|
609
546
|
return createErrorResponse(`Unknown tool: ${toolName}`, id)
|
|
610
547
|
}
|
|
611
548
|
|
|
612
|
-
// Validate input when the tool declares a Zod schema
|
|
613
549
|
let input = args
|
|
614
550
|
if (isZodSchema(customTool.inputSchema)) {
|
|
615
551
|
const parsed = customTool.inputSchema.safeParse(args)
|
|
@@ -650,8 +586,17 @@ async function handleCustomTool(
|
|
|
650
586
|
}
|
|
651
587
|
|
|
652
588
|
/**
|
|
653
|
-
*
|
|
589
|
+
* `JSON.stringify` replacer rendering a `bigint` (e.g. a `bigInt()` field's
|
|
590
|
+
* value) as a decimal string instead of throwing
|
|
591
|
+
* (`TypeError: Do not know how to serialize a BigInt`). A decimal string is
|
|
592
|
+
* the conventional JSON encoding of a 64-bit integer and is what an MCP
|
|
593
|
+
* client can consume — the field's TypeScript type stays `bigint` in
|
|
594
|
+
* application code (ADR-0029).
|
|
654
595
|
*/
|
|
596
|
+
function mcpJsonReplacer(_key: string, value: unknown): unknown {
|
|
597
|
+
return typeof value === 'bigint' ? value.toString() : value
|
|
598
|
+
}
|
|
599
|
+
|
|
655
600
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Response data structure is flexible per MCP protocol
|
|
656
601
|
function createSuccessResponse(data: any, id?: number | string): Response {
|
|
657
602
|
return new Response(
|
|
@@ -659,7 +604,7 @@ function createSuccessResponse(data: any, id?: number | string): Response {
|
|
|
659
604
|
jsonrpc: '2.0',
|
|
660
605
|
id: id ?? null,
|
|
661
606
|
result: {
|
|
662
|
-
content: [{ type: 'text', text: JSON.stringify(data,
|
|
607
|
+
content: [{ type: 'text', text: JSON.stringify(data, mcpJsonReplacer, 2) }],
|
|
663
608
|
},
|
|
664
609
|
}),
|
|
665
610
|
{
|
|
@@ -668,9 +613,6 @@ function createSuccessResponse(data: any, id?: number | string): Response {
|
|
|
668
613
|
)
|
|
669
614
|
}
|
|
670
615
|
|
|
671
|
-
/**
|
|
672
|
-
* Helper to create error response
|
|
673
|
-
*/
|
|
674
616
|
function createErrorResponse(message: string, id?: number | string): Response {
|
|
675
617
|
return new Response(
|
|
676
618
|
JSON.stringify({
|