feat(mcp): add opt-in MCP server via additive @Mcp() decorators#256
feat(mcp): add opt-in MCP server via additive @Mcp() decorators#256tobiasstrebitzer wants to merge 8 commits into
Conversation
Expose existing NestJS controllers as Model Context Protocol (MCP) tools so AI agents can drive WhatsApp as first-class tools — without replacing the controllers or taking a core framework dependency. The approach is purely additive: each tool-shaped route opts in with a single @Mcp() decorator from @silkweave/nestjs. Controllers, REST paths/ verbs, DTOs, services, guards, and the dashboard are all unchanged. MCP is off by default and only loaded when MCP_ENABLED=true, mirroring the existing QUEUE_ENABLED lazy-require pattern in app.module.ts. - 114 routes across the existing controllers decorated with @Mcp() (REST untouched). - app.module.ts: opt-in SilkweaveModule.forRoot() mounts the MCP transport at /mcp and runs the global ApiKeyGuard on tool calls via globalGuards. - Auth fully enforced over MCP: @RequireRole and per-key allowedSessions scoping both apply; throttler intentionally excluded. - Skipped non-tool-shaped routes: Prometheus metrics text and the login-only POST /api/auth/validate. - README: new "MCP" section (enablement, /mcp, auth model, additive nature). - jest: moduleNameMapper stub for the ESM-only @silkweave/nestjs so the three controller specs that import their controllers still load under the CommonJS test runner. Verified: build, lint, and unit tests green; booting with MCP_ENABLED registers 114 tools; unauthenticated tool calls are rejected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an endpoint to delete a chat from the chat list (e.g. a group you
have left), removing it via the WhatsApp client.
Endpoint: POST /api/sessions/:id/chats/delete { chatId }
- Adds deleteChat(chatId) to IWhatsAppEngine
- Implements it in the whatsapp-web-js adapter via chat.delete()
- Wires it through SessionService and SessionController
- Validates chatId as a WhatsApp JID via DeleteChatDto
- OPERATOR role required (mutation), mirroring markChatRead
- Adds unit tests mirroring sendSeen
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@rmyndharis keeping this PR's branch unchanged to keep it simple. In case you'd prefer using the Plugin system, here's the PR: |
|
Test failed due to @silkweave/nestjs being ESM-only. e2e spec imports AppModule, which transitively loads session.controller.ts and its eager import { Mcp }. Jest's CommonJS runtime can't parse the package's .mjs import statement. Fix: declare jest moduleNameMapper for @silkweave/nestjs to stub/mock. Pushing a fix |
The e2e suite imports AppModule, which transitively loads controllers that eagerly import @silkweave/nestjs. That package is ESM-only and the CommonJS Jest runtime cannot parse it. Map it to the existing no-op stub (already wired into the unit jest config) so e2e can run.
… routes Bump @silkweave/core, @silkweave/nestjs and the new optional @silkweave/mcp peer to ^2.6.0. The mcp adapter moved to a subpath export in 2.5.0, so it is now required from @silkweave/nestjs/mcp; eslint allows require() for the ESM-only @silkweave packages. 2.6.0 added boot-time warnings for @Mcp() routes whose whole-body @Body() param reflects no input fields (intersection/Partial/inline types erase to Object). Replace those with proper DTO classes so the MCP tools expose real input schemas: - SettingsController.update -> UpdateSettingsDto - InfraController.saveConfig/requestRestart/importData/importStorage Move the data-migration row interfaces into infra/dto/migration.types.ts so the controller and ImportDataDto share them without a circular import. REST behaviour is unchanged.
|
Thank you for the substantial work here — the additive After review we're going to decline this as-is, on dependency-governance grounds rather than anything about the code quality. It adds This isn't a no to MCP — it's a yes to MCP via a leaner integration. If you'd be open to an in-house-SDK variant, or want to take the plugin-system fork direction, I'd be glad to collaborate. Thank you again for the effort. |
|
Thanks @rmyndharis, appreciate the thoughtful reply. Happy to build this directly without silkweave as a dependency. Silkweave is built on the official SDK anyway, so I can take the same @Mcp() reflection approach (via Nest's DiscoveryService) and wire it straight onto the SDK. We'd also get the auth path (ApiKeyGuard + @RequireRole + per-key session scoping) test-covered. Just a heads up on deps: hono and express aren't coming from silkweave, they're direct dependencies of @modelcontextprotocol/sdk itself, so they'll come in either way. The silkweave-specific extras (pino, some CLI helpers) drop out entirely with this approach. Want me to open a fresh PR on main? Happy to put the adapter wherever fits best. |
|
Perfect — yes, please open a fresh PR against And you're right about the deps — I'll correct the record: A few things that would make this a clean merge:
Happy to collaborate on the auth-scoping design — that's the sensitive part. Thanks again for sticking with this and for the correction. |
Description
This adds an opt-in MCP server so AI agents (Claude, Cursor, etc.) can drive WhatsApp as first-class tools - list sessions, read/send messages, manage groups, webhooks, and more. It is delivered the way the maintainers asked for in the previous discussion: additively, on top of the existing NestJS controllers, without replacing them and without taking a core framework dependency.
Each tool-shaped route opts in with a single
@Mcp()decorator. No controller is deleted, no REST route/verb/path changes, DTOs/services/guards/dashboard are untouched, and MCP is off by default (it only loads whenMCP_ENABLED=true).Type of Change
Checklist
Addressing risk and technical debt
This PR is designed specifically to resolve shortcomings of previous PR #230:
1. "Core-dependency commitment - this routes the entire public API through
@silkweave/*."I have resolved this, and REST is now served entirely by stock NestJS/Express, exactly as today -
@silkweave/*is not on the request path. WhenMCP_ENABLEDis unset (the default),SilkweaveModuleis never registered: no/mcpmount, no Silkweave guards, no MCP routes. The dependency is opt-in and reversible (see Lock-in & reversibility). The public API is not coupled to Silkweave at the request-handling layer.2. "Replacing vs. adding - the PR deletes every
*.controller.ts."With this new PR, nothing is deleted. Every
@Controllerstays exactly as it is. The change is purely additive: animport { Mcp }and one@Mcp()per exposed route.@Mcp()is aSetMetadatamarker - inert at runtime unless MCP is enabled - so there is no "REST byte-for-byte unchanged" burden to verify: the REST routing is the original NestJS code, untouched. Reverting is mechanical (strip the decorators + the env-gated block).3. "v0.2.0 just landed; the branch predates new endpoints."
This branch is based on current
main(v0.2.3), post-v0.2.0. All the newer endpoints - templates, live chats,send-template, chat history,GET /infra/config, the tightened role guards - are present and exposed as MCP tools. No re-migration, no stale conflicts.4. "MCP delivered as an additive module"
MCP is now delivered as an additive module that calls the existing services without replacing the controllers or taking a core framework dependency. Instead of a parallel layer that re-declares ~100 tools and re-calls services (permanent duplication + drift), it reflects the existing controller methods, so each tool's schema, validation, and
@RequireRolestay defined in exactly one place - the controller.How it works
@silkweave/nestjsships a@Mcp()decorator and aSilkweaveModule. At boot (only when enabled), the module uses Nest'sDiscoveryServiceto find@Mcp()-decorated routes across all controllers and exposes each as an MCP tool over a Streamable-HTTP transport at/mcp. The tool name, description, and input schema are derived from the decorators the route already has (@ApiOperation,@Param/@Query/@Body,class-validator,@nestjs/swagger).Wiring stays in the existing lazy-require pattern (mirrors
QUEUE_ENABLED), so exposing a module needs noapp.module.tsor*.module.tsedits:Auth & session scoping (fully enforced over MCP)
globalGuards: [ApiKeyGuard]runs the existing API-key guard on every tool call (Silkweave's raw routes don't pick upAPP_GUARDs automatically, so it's opted in explicitly). Verified:UnauthorizedException).@RequireRole(...)enforced - e.g. aviewerkey is blocked on operator/admin tools.allowedSessionsscoping enforced - a key scoped to one session can't act on another.The login-only
POST /api/auth/validateand the Prometheusmetricstext endpoint are intentionally not exposed (not tool-shaped).Lock-in & reversibility
MCP_ENABLEDunset, there is zero MCP behavior - no/mcp, no Silkweave guards, no tool routes.app.module.tsand strip the@Mcp()decorators (a search-and-replace). No API surface, service, or dashboard change is entailed.import { Mcp }loads the package at process start (so the decorator symbol resolves), but it does nothing - no routes, no guards, no transport - unlessSilkweaveModuleis registered.What changed (and what didn't)
Changed (additive only)
@Mcp()added to 114 tool-shaped routes across the existing controllers; oneimport { Mcp }per controller.app.module.ts: an opt-in, env-gatedSilkweaveModule.forRoot(...)(same shape as the existingQUEUE_ENABLEDblock).package.json:@silkweave/core+@silkweave/nestjs(^2.2.0) added as dependencies.README.md: a short "MCP" section (enablement,/mcp, auth model, additive/off-by-default).test/mocks/silkweave-nestjs.ts+ a one-line jestmoduleNameMapper(see Testing).Deliberately unchanged
@Controllerand route handler stays; nothing deleted.ApiKeyGuard+@RequireRoleare the same decorators, now also enforced over MCP.Testing
Per the additive nature, this PR adds no new tests -
@Mcp()is metadata read only when MCP loads. One small harness change was needed: three existing controller specs (health/infra/webhook) import their controllers, which now transitively import the ESM-only@silkweave/nestjs, which the CommonJS jest runner can't load. A singlemoduleNameMapperentry maps that import to a no-opMcpstub so those specs load unchanged. Faithful, since@Mcp()is inert in unit tests.Verification
npm run build,npm run lint,npm test(416 passing) - green.MCP_ENABLED=true:tools/listreturns 114 tools across all modules; with MCP unset, no/mcproute exists and REST is unchanged.@RequireRoleand per-keyallowedSessionsscoping both enforced over MCP.Disclosure
I'm the author of Silkweave (docs on Context7), MIT-licensed. In response to the previous review, this PR reduces it from a request-layer framework to a thin, optional, off-by-default add-on that is not on the REST path and is mechanical to remove. I'm actively maintaining Silkweave and happy to own any Silkweave/MCP-related issues. Happy to collaborate further on the tool surface, auth, and session-scoping shape.
References: silkweave.dev · Context7 docs