@silkweave/nestjs 2.0.0 → 2.2.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/README.md +14 -2
- package/build/index.d.mts +37 -5
- package/build/index.d.mts.map +1 -1
- package/build/index.mjs +60 -16
- package/build/index.mjs.map +1 -1
- package/package.json +18 -11
- package/.turbo/turbo-build.log +0 -16
- package/.turbo/turbo-check.log +0 -3
- package/.turbo/turbo-lint.log +0 -2
- package/.turbo/turbo-prepack.log +0 -18
- package/eslint.config.mjs +0 -93
- package/src/adapter/mcp.ts +0 -102
- package/src/decorator/mcp.ts +0 -39
- package/src/index.ts +0 -10
- package/src/lib/controllerDiscovery.ts +0 -201
- package/src/lib/executionContext.ts +0 -76
- package/src/lib/guards.ts +0 -70
- package/src/lib/metadata.ts +0 -38
- package/src/lib/rebind.ts +0 -117
- package/src/lib/reflect/classValidator.ts +0 -56
- package/src/lib/reflect/openapi.ts +0 -87
- package/src/lib/reflect/params.ts +0 -63
- package/src/lib/reflect/route.ts +0 -58
- package/src/lib/reflect/schema.ts +0 -270
- package/src/lib/reflect/swagger.ts +0 -39
- package/src/lib/silkweave.module.ts +0 -75
- package/src/lib/types.ts +0 -61
- package/tsconfig.json +0 -25
- package/tsconfig.tsbuildinfo +0 -1
- package/tsdown.config.ts +0 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@silkweave/nestjs",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Silkweave NestJS Adapter",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -8,12 +8,23 @@
|
|
|
8
8
|
"directory": "packages/nestjs"
|
|
9
9
|
},
|
|
10
10
|
"type": "module",
|
|
11
|
-
"main": "build/index.mjs",
|
|
11
|
+
"main": "./build/index.mjs",
|
|
12
|
+
"types": "./build/index.d.mts",
|
|
13
|
+
"files": [
|
|
14
|
+
"build"
|
|
15
|
+
],
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"@silkweave/source": "./src/index.ts",
|
|
19
|
+
"types": "./build/index.d.mts",
|
|
20
|
+
"default": "./build/index.mjs"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
12
23
|
"peerDependencies": {
|
|
13
24
|
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
|
14
25
|
"@nestjs/core": "^10.0.0 || ^11.0.0",
|
|
15
26
|
"@nestjs/swagger": "^7.0.0 || ^8.0.0 || ^11.0.0",
|
|
16
|
-
"class-validator": "^0.14.0"
|
|
27
|
+
"class-validator": "^0.14.0 || ^0.15.0"
|
|
17
28
|
},
|
|
18
29
|
"peerDependenciesMeta": {
|
|
19
30
|
"@nestjs/swagger": {
|
|
@@ -27,9 +38,9 @@
|
|
|
27
38
|
"cors": "^2.8.6",
|
|
28
39
|
"express": "^5.2.1",
|
|
29
40
|
"zod": "^3.25.0",
|
|
30
|
-
"@silkweave/
|
|
31
|
-
"@silkweave/
|
|
32
|
-
"@silkweave/mcp": "2.
|
|
41
|
+
"@silkweave/core": "2.2.0",
|
|
42
|
+
"@silkweave/auth": "2.2.0",
|
|
43
|
+
"@silkweave/mcp": "2.2.0"
|
|
33
44
|
},
|
|
34
45
|
"devDependencies": {
|
|
35
46
|
"@eslint/js": "^10.0.1",
|
|
@@ -56,9 +67,5 @@
|
|
|
56
67
|
"typecheck": "tsc --noEmit",
|
|
57
68
|
"lint": "eslint",
|
|
58
69
|
"check": "pnpm lint && pnpm typecheck"
|
|
59
|
-
}
|
|
60
|
-
"exports": {
|
|
61
|
-
".": "./build/index.mjs"
|
|
62
|
-
},
|
|
63
|
-
"types": "./build/index.d.ts"
|
|
70
|
+
}
|
|
64
71
|
}
|
package/.turbo/turbo-build.log
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
[2m$ tsdown[22m
|
|
2
|
-
[34mℹ[39m [34mtsdown v0.21.10[39m powered by [38;2;255;126;23mrolldown v1.0.0-rc.17[39m
|
|
3
|
-
[34mℹ[39m config file: [4m/Users/atomic/projects/silkweave/silkweave/packages/nestjs/tsdown.config.ts[24m
|
|
4
|
-
[34mℹ[39m entry: [34msrc/index.ts[39m
|
|
5
|
-
[34mℹ[39m tsconfig: [34mtsconfig.json[39m
|
|
6
|
-
[34mℹ[39m Build start
|
|
7
|
-
[34mℹ[39m Hint: consider adding [34mdeps.onlyBundle[39m option to avoid unintended bundling of dependencies, or set [34mdeps.onlyBundle: false[39m to disable this hint.
|
|
8
|
-
See more at [4mhttps://tsdown.dev/options/dependencies#deps-onlybundle[24m
|
|
9
|
-
Detected dependencies in bundle:
|
|
10
|
-
- [34mrxjs[39m
|
|
11
|
-
[34mℹ[39m [2mbuild/[22m[1mindex.mjs[22m [2m339.14 kB[22m [2m│ gzip: 49.99 kB[22m
|
|
12
|
-
[34mℹ[39m [2mbuild/[22mindex.mjs.map [2m579.22 kB[22m [2m│ gzip: 88.28 kB[22m
|
|
13
|
-
[34mℹ[39m [2mbuild/[22mindex.d.mts.map [2m 3.06 kB[22m [2m│ gzip: 1.10 kB[22m
|
|
14
|
-
[34mℹ[39m [2mbuild/[22m[32m[1mindex.d.mts[22m[39m [2m 17.08 kB[22m [2m│ gzip: 6.02 kB[22m
|
|
15
|
-
[34mℹ[39m 4 files, total: 938.50 kB
|
|
16
|
-
[32m✔[39m Build complete in [32m1034ms[39m
|
package/.turbo/turbo-check.log
DELETED
package/.turbo/turbo-lint.log
DELETED
package/.turbo/turbo-prepack.log
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
[2m$ pnpm clean && pnpm build[22m
|
|
2
|
-
[2m$ rimraf build[22m
|
|
3
|
-
[2m$ tsdown[22m
|
|
4
|
-
[34mℹ[39m [34mtsdown v0.21.10[39m powered by [38;2;255;126;23mrolldown v1.0.0-rc.17[39m
|
|
5
|
-
[34mℹ[39m config file: [4m/Users/atomic/projects/silkweave/silkweave/packages/nestjs/tsdown.config.ts[24m
|
|
6
|
-
[34mℹ[39m entry: [34msrc/index.ts[39m
|
|
7
|
-
[34mℹ[39m tsconfig: [34mtsconfig.json[39m
|
|
8
|
-
[34mℹ[39m Build start
|
|
9
|
-
[34mℹ[39m Hint: consider adding [34mdeps.onlyBundle[39m option to avoid unintended bundling of dependencies, or set [34mdeps.onlyBundle: false[39m to disable this hint.
|
|
10
|
-
See more at [4mhttps://tsdown.dev/options/dependencies#deps-onlybundle[24m
|
|
11
|
-
Detected dependencies in bundle:
|
|
12
|
-
- [34mrxjs[39m
|
|
13
|
-
[34mℹ[39m [2mbuild/[22m[1mindex.mjs[22m [2m344.18 kB[22m [2m│ gzip: 51.78 kB[22m
|
|
14
|
-
[34mℹ[39m [2mbuild/[22mindex.mjs.map [2m589.05 kB[22m [2m│ gzip: 91.22 kB[22m
|
|
15
|
-
[34mℹ[39m [2mbuild/[22mindex.d.mts.map [2m 3.45 kB[22m [2m│ gzip: 1.22 kB[22m
|
|
16
|
-
[34mℹ[39m [2mbuild/[22m[32m[1mindex.d.mts[22m[39m [2m 20.10 kB[22m [2m│ gzip: 6.89 kB[22m
|
|
17
|
-
[34mℹ[39m 4 files, total: 956.79 kB
|
|
18
|
-
[32m✔[39m Build complete in [32m822ms[39m
|
package/eslint.config.mjs
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import eslint from '@eslint/js'
|
|
2
|
-
import stylistic from '@stylistic/eslint-plugin'
|
|
3
|
-
import { defineConfig } from 'eslint/config'
|
|
4
|
-
import tseslint from 'typescript-eslint'
|
|
5
|
-
|
|
6
|
-
export default defineConfig(eslint.configs.recommended, tseslint.configs.recommendedTypeChecked, tseslint.configs.stylisticTypeChecked, {
|
|
7
|
-
languageOptions: {
|
|
8
|
-
parserOptions: {
|
|
9
|
-
project: 'tsconfig.json',
|
|
10
|
-
tsconfigRootDir: import.meta.dirname
|
|
11
|
-
}
|
|
12
|
-
},
|
|
13
|
-
plugins: { '@stylistic': stylistic },
|
|
14
|
-
rules: {
|
|
15
|
-
'@stylistic/space-in-parens': ['error'],
|
|
16
|
-
'@stylistic/comma-spacing': ['error'],
|
|
17
|
-
'@stylistic/no-multi-spaces': ['error'],
|
|
18
|
-
'@stylistic/no-trailing-spaces': ['error'],
|
|
19
|
-
'@stylistic/no-whitespace-before-property': ['error'],
|
|
20
|
-
'@stylistic/array-bracket-newline': ['error', 'consistent'],
|
|
21
|
-
'@stylistic/array-bracket-spacing': ['error'],
|
|
22
|
-
'@stylistic/arrow-spacing': ['error'],
|
|
23
|
-
'@stylistic/arrow-parens': ['error', 'always'],
|
|
24
|
-
'@stylistic/block-spacing': ['error', 'always'],
|
|
25
|
-
'@stylistic/brace-style': ['error', '1tbs', { 'allowSingleLine': true }],
|
|
26
|
-
'@stylistic/comma-dangle': ['error', 'never'],
|
|
27
|
-
'@stylistic/key-spacing': ['error'],
|
|
28
|
-
'@stylistic/keyword-spacing': ['error'],
|
|
29
|
-
'@stylistic/member-delimiter-style': ['error', { 'multiline': { 'delimiter': 'none' } }],
|
|
30
|
-
'@stylistic/no-extra-semi': ['error'],
|
|
31
|
-
'@stylistic/indent': ['error', 2],
|
|
32
|
-
'@stylistic/no-multiple-empty-lines': ['error', { 'max': 1, 'maxEOF': 0, 'maxBOF': 0 }],
|
|
33
|
-
'@stylistic/object-curly-spacing': ['error', 'always'],
|
|
34
|
-
'@stylistic/quotes': ['error', 'single'],
|
|
35
|
-
'@stylistic/semi': ['error', 'never'],
|
|
36
|
-
'@stylistic/space-before-blocks': ['error', 'always'],
|
|
37
|
-
'@stylistic/space-before-function-paren': ['error', { 'anonymous': 'always', 'named': 'never', 'asyncArrow': 'always' }],
|
|
38
|
-
'@typescript-eslint/adjacent-overload-signatures': 'error',
|
|
39
|
-
'@typescript-eslint/array-type': 'off',
|
|
40
|
-
'@typescript-eslint/await-thenable': 'error',
|
|
41
|
-
'@typescript-eslint/ban-types': 'off',
|
|
42
|
-
'@typescript-eslint/consistent-type-assertions': 'off',
|
|
43
|
-
'@typescript-eslint/explicit-function-return-type': 'off',
|
|
44
|
-
'@typescript-eslint/explicit-member-accessibility': 'off',
|
|
45
|
-
'@typescript-eslint/no-angle-bracket-type-assertion': 'off',
|
|
46
|
-
'@typescript-eslint/no-empty-function': 'off',
|
|
47
|
-
'@typescript-eslint/no-empty-interface': 'off',
|
|
48
|
-
'@typescript-eslint/no-extra-non-null-assertion': 'error',
|
|
49
|
-
'@typescript-eslint/no-floating-promises': 'off',
|
|
50
|
-
'@typescript-eslint/no-misused-new': 'error',
|
|
51
|
-
'@typescript-eslint/no-misused-promises': 'off',
|
|
52
|
-
'@typescript-eslint/no-namespace': 'off',
|
|
53
|
-
'@typescript-eslint/no-non-null-asserted-optional-chain': 'error',
|
|
54
|
-
'@typescript-eslint/no-non-null-assertion': 'off',
|
|
55
|
-
'@typescript-eslint/no-object-literal-type-assertion': 'off',
|
|
56
|
-
'@typescript-eslint/no-parameter-properties': 'off',
|
|
57
|
-
'@typescript-eslint/no-shadow': 'error',
|
|
58
|
-
'@typescript-eslint/no-triple-slash-reference': 'off',
|
|
59
|
-
'@typescript-eslint/no-unused-vars': ['error', {
|
|
60
|
-
'args': 'all',
|
|
61
|
-
'argsIgnorePattern': '^_',
|
|
62
|
-
'caughtErrors': 'all',
|
|
63
|
-
'caughtErrorsIgnorePattern': '^_',
|
|
64
|
-
'destructuredArrayIgnorePattern': '^_',
|
|
65
|
-
'varsIgnorePattern': '^_',
|
|
66
|
-
'ignoreRestSiblings': true
|
|
67
|
-
}],
|
|
68
|
-
'@typescript-eslint/no-use-before-define': 'off',
|
|
69
|
-
'@typescript-eslint/no-var-requires': 'off',
|
|
70
|
-
'@typescript-eslint/prefer-for-of': 'error',
|
|
71
|
-
'@typescript-eslint/prefer-interface': 'off',
|
|
72
|
-
'@typescript-eslint/prefer-nullish-coalescing': 'off',
|
|
73
|
-
'@typescript-eslint/prefer-optional-chain': 'error',
|
|
74
|
-
'@typescript-eslint/return-await': 'error',
|
|
75
|
-
'@typescript-eslint/unified-signatures': 'error',
|
|
76
|
-
'@typescript-eslint/no-unsafe-return': 'off',
|
|
77
|
-
'@typescript-eslint/no-redundant-type-constituents': 'off',
|
|
78
|
-
'@typescript-eslint/no-unsafe-member-access': 'off',
|
|
79
|
-
'@typescript-eslint/no-unsafe-call': 'off',
|
|
80
|
-
'@typescript-eslint/no-unsafe-argument': 'off',
|
|
81
|
-
'@typescript-eslint/dot-notation': 'off',
|
|
82
|
-
'@typescript-eslint/prefer-regexp-exec': 'off',
|
|
83
|
-
'@typescript-eslint/require-await': 'off',
|
|
84
|
-
'@typescript-eslint/only-throw-error': 'error',
|
|
85
|
-
'@typescript-eslint/no-base-to-string': 'off',
|
|
86
|
-
'@typescript-eslint/restrict-template-expressions': 'off'
|
|
87
|
-
}
|
|
88
|
-
}, {
|
|
89
|
-
files: ['eslint.config.mjs'],
|
|
90
|
-
extends: [tseslint.configs.disableTypeChecked]
|
|
91
|
-
}, {
|
|
92
|
-
ignores: ['build', 'tsdown.config.ts', 'scratch']
|
|
93
|
-
})
|
package/src/adapter/mcp.ts
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { AuthConfig } from '@silkweave/auth'
|
|
2
|
-
import {
|
|
3
|
-
authMiddleware,
|
|
4
|
-
mcpCors,
|
|
5
|
-
mcpTransport,
|
|
6
|
-
oauthRoutes,
|
|
7
|
-
protectedResourceMetadata,
|
|
8
|
-
sideloadResource
|
|
9
|
-
} from '@silkweave/mcp'
|
|
10
|
-
import { type CorsOptions } from 'cors'
|
|
11
|
-
import express, { type RequestHandler } from 'express'
|
|
12
|
-
import type { NestAdapterRegisterContext, NestSilkweaveAdapter } from '../lib/types.js'
|
|
13
|
-
|
|
14
|
-
export interface McpAdapterOptions {
|
|
15
|
-
/** URL prefix the MCP namespace lives under - the transport itself is at this exact path. Default `'/mcp'`. */
|
|
16
|
-
basePath?: string
|
|
17
|
-
/** Optional bearer-token / OAuth 2.1 config. */
|
|
18
|
-
auth?: AuthConfig
|
|
19
|
-
/** CORS configuration. `false` to disable, `true`/omitted for permissive defaults, or a `CorsOptions` object. */
|
|
20
|
-
cors?: CorsOptions | boolean
|
|
21
|
-
/** Mount the sideload resource route at `${basePath}/resource/:id`. Default `true`. */
|
|
22
|
-
sideloadResources?: boolean
|
|
23
|
-
/** Directory the sideload route reads from. Default `'resources'`. */
|
|
24
|
-
resourceDir?: string
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function compose(...handlers: RequestHandler[]): RequestHandler {
|
|
28
|
-
return (req, res, next) => {
|
|
29
|
-
let i = 0
|
|
30
|
-
const dispatch: () => void = () => {
|
|
31
|
-
const h = handlers[i++]
|
|
32
|
-
if (!h) { return next() }
|
|
33
|
-
h(req, res, (err) => err ? next(err) : dispatch())
|
|
34
|
-
}
|
|
35
|
-
dispatch()
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* MCP adapter for `@silkweave/nestjs`. Registers the MCP Streamable HTTP
|
|
41
|
-
* transport, sideload, well-known and OAuth routes individually on Nest's
|
|
42
|
-
* HTTP adapter at the configured `basePath` (default `/mcp`):
|
|
43
|
-
*
|
|
44
|
-
* - `POST/GET/DELETE ${basePath}` - Streamable HTTP transport
|
|
45
|
-
* - `GET ${basePath}/resource/:id` - sideload (`sideloadResources` opt-out)
|
|
46
|
-
* - `GET ${basePath}/.well-known/oauth-protected-resource` - RFC 9728 metadata (when `auth.resourceUrl`/`auth.authorizationServers` set)
|
|
47
|
-
* - `GET ${basePath}/authorize`, `POST ${basePath}/token`, `POST ${basePath}/register`, `GET ${basePath}${callbackPath}` (when `auth.provider` set)
|
|
48
|
-
*
|
|
49
|
-
* Each route is a real Nest-level route - they show up in
|
|
50
|
-
* `RoutesResolver`'s log and there is no sub-app or middleware-slot
|
|
51
|
-
* indirection.
|
|
52
|
-
*/
|
|
53
|
-
export function mcp(options: McpAdapterOptions = {}): NestSilkweaveAdapter {
|
|
54
|
-
return {
|
|
55
|
-
name: 'mcp',
|
|
56
|
-
register({ httpAdapter, silkweaveOptions, baseContext, actions }: NestAdapterRegisterContext): void {
|
|
57
|
-
const basePath = (options.basePath ?? '/mcp').replace(/\/$/, '')
|
|
58
|
-
if (!basePath) { throw new Error('@silkweave/nestjs mcp(): basePath cannot be empty or "/" - pick a path like "/mcp".') }
|
|
59
|
-
|
|
60
|
-
const adapter = httpAdapter as unknown as {
|
|
61
|
-
get: (path: string, ...h: RequestHandler[]) => unknown
|
|
62
|
-
post: (path: string, ...h: RequestHandler[]) => unknown
|
|
63
|
-
delete: (path: string, ...h: RequestHandler[]) => unknown
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const corsHandler = mcpCors(options.cors ?? true)
|
|
67
|
-
const auth = options.auth
|
|
68
|
-
const guard = auth ? authMiddleware(auth, baseContext) : null
|
|
69
|
-
const prefix = (...handlers: (RequestHandler | null)[]): RequestHandler[] =>
|
|
70
|
-
handlers.filter((h): h is RequestHandler => Boolean(h))
|
|
71
|
-
|
|
72
|
-
// Public auth-discovery / OAuth routes - registered first so they're
|
|
73
|
-
// never inadvertently guarded by the auth middleware.
|
|
74
|
-
if (auth?.authorizationServers?.length && auth.resourceUrl) {
|
|
75
|
-
adapter.get(
|
|
76
|
-
`${basePath}/.well-known/oauth-protected-resource`,
|
|
77
|
-
...prefix(corsHandler, protectedResourceMetadata(auth))
|
|
78
|
-
)
|
|
79
|
-
}
|
|
80
|
-
if (auth?.provider) {
|
|
81
|
-
const oauth = oauthRoutes(auth)
|
|
82
|
-
adapter.get(`${basePath}/.well-known/oauth-authorization-server`, ...prefix(corsHandler, oauth.wellKnownAuthServer))
|
|
83
|
-
adapter.get(`${basePath}/authorize`, ...prefix(corsHandler, oauth.authorize))
|
|
84
|
-
adapter.get(`${basePath}${oauth.callbackPath}`, ...prefix(corsHandler, oauth.callback))
|
|
85
|
-
adapter.post(`${basePath}/token`, ...prefix(corsHandler, ...oauth.token))
|
|
86
|
-
adapter.post(`${basePath}/register`, ...prefix(corsHandler, ...oauth.register))
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Protected routes - wrapped with auth middleware (when configured).
|
|
90
|
-
const protect = guard ? (h: RequestHandler) => compose(guard, h) : (h: RequestHandler) => h
|
|
91
|
-
|
|
92
|
-
if (options.sideloadResources !== false) {
|
|
93
|
-
adapter.get(`${basePath}/resource/:id`, ...prefix(corsHandler, protect(sideloadResource({ resourceDir: options.resourceDir }))))
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const transport = mcpTransport(silkweaveOptions, baseContext, actions)
|
|
97
|
-
adapter.post(basePath, ...prefix(corsHandler, express.json(), protect(transport.post)))
|
|
98
|
-
adapter.get(basePath, ...prefix(corsHandler, protect(transport.stream)))
|
|
99
|
-
adapter.delete(basePath, ...prefix(corsHandler, protect(transport.stream)))
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
package/src/decorator/mcp.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { SetMetadata } from '@nestjs/common'
|
|
2
|
-
import { MCP_METADATA, type McpMetadata } from '../lib/metadata.js'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Method decorator that exposes an existing NestJS controller route as an MCP
|
|
6
|
-
* tool. It is **additive** - the route keeps serving HTTP exactly as before;
|
|
7
|
-
* `@Mcp()` just opts the method into MCP discovery.
|
|
8
|
-
*
|
|
9
|
-
* The tool's name, description, and input schema are reflected from the
|
|
10
|
-
* method's own metadata:
|
|
11
|
-
* - **fields** from the parameter decorators (`@Param`/`@Query`/`@Body`) - a
|
|
12
|
-
* `@Param('id')` becomes an `id` field; a whole-DTO `@Body() dto: CreateDto`
|
|
13
|
-
* is flattened to its properties,
|
|
14
|
-
* - **types/constraints/descriptions** from `@nestjs/swagger`
|
|
15
|
-
* (`@ApiParam`/`@ApiQuery`/`@ApiProperty`/`@ApiOperation`) and, when present,
|
|
16
|
-
* `class-validator` decorators on the DTOs,
|
|
17
|
-
* - optionally refined by an OpenAPI document passed to `SilkweaveModule`.
|
|
18
|
-
*
|
|
19
|
-
* On a tool call the input is split back into the method's positional arguments
|
|
20
|
-
* and the method is invoked directly (with `@UseGuards` guards applied first).
|
|
21
|
-
*
|
|
22
|
-
* @example
|
|
23
|
-
* ```ts
|
|
24
|
-
* @Controller('sessions/:sessionId/channels')
|
|
25
|
-
* export class ChannelsController {
|
|
26
|
-
* @Get(':channelId')
|
|
27
|
-
* @ApiOperation({ summary: 'Get a specific channel by ID' })
|
|
28
|
-
* @ApiParam({ name: 'sessionId', description: 'Session ID' })
|
|
29
|
-
* @ApiParam({ name: 'channelId', description: 'Channel ID' })
|
|
30
|
-
* @Mcp()
|
|
31
|
-
* findOne(@Param('sessionId') sessionId: string, @Param('channelId') channelId: string) {
|
|
32
|
-
* return this.service.get(sessionId, channelId)
|
|
33
|
-
* }
|
|
34
|
-
* }
|
|
35
|
-
* ```
|
|
36
|
-
*/
|
|
37
|
-
export function Mcp(options: McpMetadata = {}): MethodDecorator {
|
|
38
|
-
return SetMetadata(MCP_METADATA, options)
|
|
39
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export * from './adapter/mcp.js'
|
|
2
|
-
export * from './decorator/mcp.js'
|
|
3
|
-
export * from './lib/controllerDiscovery.js'
|
|
4
|
-
export * from './lib/guards.js'
|
|
5
|
-
export * from './lib/metadata.js'
|
|
6
|
-
export * from './lib/rebind.js'
|
|
7
|
-
export * from './lib/silkweave.module.js'
|
|
8
|
-
export * from './lib/types.js'
|
|
9
|
-
export * from './lib/reflect/openapi.js'
|
|
10
|
-
export * from './lib/reflect/schema.js'
|
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
|
2
|
-
import { Injectable, type Type } from '@nestjs/common'
|
|
3
|
-
import { DiscoveryService, MetadataScanner, ModuleRef, Reflector } from '@nestjs/core'
|
|
4
|
-
import { createAction, type Action, type SilkweaveContext } from '@silkweave/core'
|
|
5
|
-
import { z } from 'zod/v4'
|
|
6
|
-
import { collectGuards, runGuards } from './guards.js'
|
|
7
|
-
import { MCP_METADATA, type McpMetadata } from './metadata.js'
|
|
8
|
-
import { invokeRebound, specialBinding, type Binding } from './rebind.js'
|
|
9
|
-
import { buildOpenApiLookup, openApiFields, type OpenApiDocument, type OpenApiLookup } from './reflect/openapi.js'
|
|
10
|
-
import { PARAMTYPE, type ParamSlot, readParamSlots } from './reflect/params.js'
|
|
11
|
-
import { reflectRoute } from './reflect/route.js'
|
|
12
|
-
import { type FieldDesc, fieldToZod, mergeField, reflectDtoFields } from './reflect/schema.js'
|
|
13
|
-
import { reflectOperation } from './reflect/swagger.js'
|
|
14
|
-
|
|
15
|
-
interface DiscoveredMcp {
|
|
16
|
-
instance: object
|
|
17
|
-
classRef: Type<unknown>
|
|
18
|
-
method: (...args: unknown[]) => unknown
|
|
19
|
-
methodName: string
|
|
20
|
-
meta: McpMetadata
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface BuiltInput {
|
|
24
|
-
shape: Record<string, z.ZodType>
|
|
25
|
-
bindings: Binding[]
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
@Injectable()
|
|
29
|
-
export class ControllerDiscovery {
|
|
30
|
-
constructor(
|
|
31
|
-
private readonly discovery: DiscoveryService,
|
|
32
|
-
private readonly scanner: MetadataScanner,
|
|
33
|
-
private readonly reflector: Reflector,
|
|
34
|
-
private readonly moduleRef: ModuleRef
|
|
35
|
-
) { }
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Walk every Nest provider/controller, find methods annotated with `@Mcp`,
|
|
39
|
-
* and build a core `Action` per method whose input schema is reflected from
|
|
40
|
-
* the route + parameter decorators (+ optional OpenAPI document) and whose
|
|
41
|
-
* `run` re-binds the validated input back into the method's positional
|
|
42
|
-
* arguments (with `@UseGuards` guards applied first).
|
|
43
|
-
*/
|
|
44
|
-
discover(openapi?: OpenApiDocument): Action[] {
|
|
45
|
-
const lookup = openapi ? buildOpenApiLookup(openapi) : undefined
|
|
46
|
-
const discovered: DiscoveredMcp[] = []
|
|
47
|
-
for (const wrapper of this.discovery.getProviders().concat(this.discovery.getControllers())) {
|
|
48
|
-
const { instance } = wrapper
|
|
49
|
-
if (!instance || typeof instance !== 'object') { continue }
|
|
50
|
-
const proto = Object.getPrototypeOf(instance) as object | null
|
|
51
|
-
if (!proto) { continue }
|
|
52
|
-
const classRef = instance.constructor as Type<unknown>
|
|
53
|
-
for (const methodName of this.scanner.getAllMethodNames(proto)) {
|
|
54
|
-
const method = (proto as Record<string, unknown>)[methodName] as ((...args: unknown[]) => unknown) | undefined
|
|
55
|
-
if (typeof method !== 'function') { continue }
|
|
56
|
-
const meta = this.reflector.get<McpMetadata>(MCP_METADATA, method)
|
|
57
|
-
if (!meta) { continue }
|
|
58
|
-
discovered.push({ instance, classRef, method, methodName, meta })
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return discovered.map((d) => this.toAction(d, lookup))
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
private toAction(d: DiscoveredMcp, lookup: OpenApiLookup | undefined): Action {
|
|
65
|
-
const proto = Object.getPrototypeOf(d.instance) as object
|
|
66
|
-
const route = reflectRoute(d.classRef, d.method)
|
|
67
|
-
const slots = readParamSlots(d.classRef, d.methodName, proto)
|
|
68
|
-
const operation = reflectOperation(d.method)
|
|
69
|
-
const docFields = lookup ? openApiFields(lookup, route.method, route.openapiPath) : {}
|
|
70
|
-
|
|
71
|
-
const { shape, bindings } = this.buildInput(d, route.pathParams, slots, operation.params, docFields)
|
|
72
|
-
|
|
73
|
-
const base = d.classRef.name.replace(/Controller$/, '')
|
|
74
|
-
const name = d.meta.name ?? `${base}.${d.methodName}`
|
|
75
|
-
const description = d.meta.description ?? operation.description ?? `${d.methodName} (${route.method} /${route.path})`
|
|
76
|
-
const applyParamPipes = d.meta.pipes !== 'skip'
|
|
77
|
-
|
|
78
|
-
const guards = collectGuards(this.reflector, d.classRef, d.method)
|
|
79
|
-
const { moduleRef, reflector } = this
|
|
80
|
-
const classRef = d.classRef
|
|
81
|
-
const method = d.method
|
|
82
|
-
const instance = d.instance
|
|
83
|
-
|
|
84
|
-
const applyGuards = async (context: SilkweaveContext): Promise<void> => {
|
|
85
|
-
if (guards.length === 0) { return }
|
|
86
|
-
const request = context.getOptional<unknown>('request')
|
|
87
|
-
const response = context.getOptional<unknown>('response') ?? null
|
|
88
|
-
const hasRequest = request != null
|
|
89
|
-
const guardRequest = hasRequest ? request : { headers: {}, params: {}, query: {} }
|
|
90
|
-
await runGuards(guards, moduleRef, reflector, classRef, method, guardRequest, response, hasRequest ? 'http' : 'rpc')
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return createAction({
|
|
94
|
-
name,
|
|
95
|
-
description,
|
|
96
|
-
input: z.object(shape),
|
|
97
|
-
// Only the MCP adapter exposes `@Mcp` methods; a future `@Trpc` decorator
|
|
98
|
-
// would tag its own actions for the tRPC adapter.
|
|
99
|
-
isEnabled: (ctx) => ctx.getOptional<string>('adapter') === 'mcp',
|
|
100
|
-
run: async (input: object, context: SilkweaveContext): Promise<object> => {
|
|
101
|
-
await applyGuards(context)
|
|
102
|
-
const request = context.getOptional<{ headers?: Record<string, unknown> }>('request')
|
|
103
|
-
const result = await invokeRebound(method, instance, input as Record<string, unknown>, bindings, request, applyParamPipes)
|
|
104
|
-
return (result ?? {})
|
|
105
|
-
}
|
|
106
|
-
}) as Action
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** Build the merged Zod input shape and the per-argument re-bind plan. */
|
|
110
|
-
private buildInput(
|
|
111
|
-
d: DiscoveredMcp,
|
|
112
|
-
pathParams: string[],
|
|
113
|
-
slots: ReturnType<typeof readParamSlots>,
|
|
114
|
-
operationParams: Record<string, FieldDesc>,
|
|
115
|
-
docFields: Record<string, FieldDesc>
|
|
116
|
-
): BuiltInput {
|
|
117
|
-
const proto = Object.getPrototypeOf(d.instance) as object
|
|
118
|
-
const designTypes = (Reflect.getMetadata('design:paramtypes', proto, d.methodName) as unknown[] | undefined) ?? []
|
|
119
|
-
const fields: Record<string, FieldDesc> = {}
|
|
120
|
-
const maxIndex = slots.reduce((m, s) => Math.max(m, s.index), -1)
|
|
121
|
-
const bindings: Binding[] = Array.from({ length: maxIndex + 1 }, () => ({ kind: 'missing' as const }))
|
|
122
|
-
|
|
123
|
-
const addField = (name: string, desc: FieldDesc): void => {
|
|
124
|
-
fields[name] = name in fields ? mergeField(fields[name], desc) : desc
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
for (const slot of slots) {
|
|
128
|
-
const { binding, fields: contributed } = contributeSlot(slot, pathParams, designTypes)
|
|
129
|
-
bindings[slot.index] = binding
|
|
130
|
-
for (const [name, desc] of Object.entries(contributed)) { addField(name, desc) }
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Layer operation-level (`@ApiParam`/`@ApiQuery`) then OpenAPI-document
|
|
134
|
-
// metadata over the structural fields (later sources win per field).
|
|
135
|
-
for (const [name, desc] of Object.entries(operationParams)) {
|
|
136
|
-
if (name in fields) { fields[name] = mergeField(fields[name], desc) }
|
|
137
|
-
}
|
|
138
|
-
for (const [name, desc] of Object.entries(docFields)) {
|
|
139
|
-
if (name in fields) { fields[name] = mergeField(fields[name], desc) }
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const shape: Record<string, z.ZodType> = {}
|
|
143
|
-
for (const [name, desc] of Object.entries(fields)) { shape[name] = fieldToZod(desc) }
|
|
144
|
-
// `@Mcp({ input })` raw-shape override wins per field.
|
|
145
|
-
Object.assign(shape, d.meta.input ?? {})
|
|
146
|
-
|
|
147
|
-
return { shape, bindings }
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function designTypeAt(designTypes: unknown[], index: number): FieldDesc {
|
|
152
|
-
const ctor = designTypes[index]
|
|
153
|
-
if (ctor === String) { return { type: 'string' } }
|
|
154
|
-
if (ctor === Number) { return { type: 'number' } }
|
|
155
|
-
if (ctor === Boolean) { return { type: 'boolean' } }
|
|
156
|
-
return {}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
interface SlotContribution {
|
|
160
|
-
binding: Binding
|
|
161
|
-
/** Input fields this slot contributes, keyed by field name. */
|
|
162
|
-
fields: Record<string, FieldDesc>
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** A `@Param('id')` scalar or a bare `@Param()` covering all path params. */
|
|
166
|
-
function paramContribution(slot: ParamSlot, pathParams: string[]): SlotContribution {
|
|
167
|
-
if (slot.data) {
|
|
168
|
-
return {
|
|
169
|
-
binding: { kind: 'value', field: slot.data, source: 'path', metatype: slot.designType, pipes: slot.pipes },
|
|
170
|
-
fields: { [slot.data]: { type: 'string', required: true } }
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
const fields: Record<string, FieldDesc> = {}
|
|
174
|
-
for (const p of pathParams) { fields[p] = { type: 'string', required: true } }
|
|
175
|
-
return { binding: { kind: 'params', fields: pathParams }, fields }
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/** A `@Query('x')`/`@Body('x')` scalar or a whole-DTO `@Query()`/`@Body()`. */
|
|
179
|
-
function bodyOrQueryContribution(slot: ParamSlot, source: 'query' | 'body', requiredScalar: boolean, designTypes: unknown[]): SlotContribution {
|
|
180
|
-
if (slot.data) {
|
|
181
|
-
return {
|
|
182
|
-
binding: { kind: 'value', field: slot.data, source, metatype: slot.designType, pipes: slot.pipes },
|
|
183
|
-
fields: { [slot.data]: mergeField(designTypeAt(designTypes, slot.index), { required: requiredScalar }) }
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
const dtoFields = reflectDtoFields(slot.designType)
|
|
187
|
-
return {
|
|
188
|
-
binding: { kind: 'object', source, fields: Object.keys(dtoFields), metatype: slot.designType, pipes: slot.pipes },
|
|
189
|
-
fields: dtoFields
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/** Map one parameter slot to its input-field contribution and re-bind instruction. */
|
|
194
|
-
function contributeSlot(slot: ParamSlot, pathParams: string[], designTypes: unknown[]): SlotContribution {
|
|
195
|
-
switch (slot.paramtype) {
|
|
196
|
-
case PARAMTYPE.PARAM: return paramContribution(slot, pathParams)
|
|
197
|
-
case PARAMTYPE.QUERY: return bodyOrQueryContribution(slot, 'query', false, designTypes)
|
|
198
|
-
case PARAMTYPE.BODY: return bodyOrQueryContribution(slot, 'body', true, designTypes)
|
|
199
|
-
default: return { binding: specialBinding(slot.paramtype, slot.data) ?? { kind: 'missing' }, fields: {} }
|
|
200
|
-
}
|
|
201
|
-
}
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import type { ArgumentsHost, ContextType, ExecutionContext, Type } from '@nestjs/common'
|
|
2
|
-
|
|
3
|
-
/** Subset of `HttpArgumentsHost` we need - re-declared inline to avoid deep `@nestjs/common/interfaces` imports. */
|
|
4
|
-
interface HttpHost {
|
|
5
|
-
getRequest<T = unknown>(): T
|
|
6
|
-
getResponse<T = unknown>(): T
|
|
7
|
-
getNext<T = unknown>(): T
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
interface RpcHost {
|
|
11
|
-
getData<T = unknown>(): T
|
|
12
|
-
getContext<T = unknown>(): T
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
interface WsHost {
|
|
16
|
-
getClient<T = unknown>(): T
|
|
17
|
-
getData<T = unknown>(): T
|
|
18
|
-
getPattern(): string
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Minimal `ExecutionContext` impl Nest guards can consume. We only need
|
|
23
|
-
* `switchToHttp()` to work for our HTTP-backed transports; the RPC/WS shims are
|
|
24
|
-
* stubbed so guards that introspect type can still call them without crashing.
|
|
25
|
-
*/
|
|
26
|
-
export class SilkweaveExecutionContext implements ExecutionContext, ArgumentsHost {
|
|
27
|
-
constructor(
|
|
28
|
-
private readonly args: unknown[],
|
|
29
|
-
private readonly classRef: Type<unknown>,
|
|
30
|
-
private readonly handler: (...handlerArgs: unknown[]) => unknown,
|
|
31
|
-
private readonly contextType = 'http'
|
|
32
|
-
) { }
|
|
33
|
-
|
|
34
|
-
getType<T extends string = ContextType>(): T {
|
|
35
|
-
return this.contextType as T
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
getClass<T = unknown>(): Type<T> {
|
|
39
|
-
return this.classRef as Type<T>
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
getHandler(): (...handlerArgs: unknown[]) => unknown {
|
|
43
|
-
return this.handler
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
getArgs<T extends unknown[] = unknown[]>(): T {
|
|
47
|
-
return this.args as T
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
getArgByIndex<T = unknown>(index: number): T {
|
|
51
|
-
return this.args[index] as T
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
switchToHttp(): HttpHost {
|
|
55
|
-
return {
|
|
56
|
-
getRequest: <T = unknown>(): T => this.args[0] as T,
|
|
57
|
-
getResponse: <T = unknown>(): T => this.args[1] as T,
|
|
58
|
-
getNext: <T = unknown>(): T => this.args[2] as T
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
switchToRpc(): RpcHost {
|
|
63
|
-
return {
|
|
64
|
-
getData: <T = unknown>(): T => this.args[0] as T,
|
|
65
|
-
getContext: <T = unknown>(): T => this.args[1] as T
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
switchToWs(): WsHost {
|
|
70
|
-
return {
|
|
71
|
-
getClient: <T = unknown>(): T => this.args[0] as T,
|
|
72
|
-
getData: <T = unknown>(): T => this.args[1] as T,
|
|
73
|
-
getPattern: (): string => ''
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|