@loopstack/mcp-module 0.3.0 → 0.3.2

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 CHANGED
@@ -1,160 +1,169 @@
1
- # @loopstack/mcp-module
2
-
3
- > Remote-MCP client tools for the [Loopstack AI](https://loopstack.ai) automation framework.
4
-
5
- Lets a Loopstack agent list and call tools on remote Model Context Protocol (MCP)
6
- servers over HTTPS — Streamable HTTP or legacy SSE — with a strict SSRF allowlist
7
- and zero-trust handling of authentication secrets.
8
-
9
- ## What this is (and isn't)
10
-
11
- - **Is:** a _client_ module — your Loopstack app reaches _out_ to a remote MCP
12
- server (Linear, GitHub, internal tools, etc.).
13
- - **Isn't:** an MCP _server_ — it does not expose your workflows over MCP.
14
-
15
- ## Tools
1
+ ---
2
+ title: MCP Module
3
+ description: Remote MCP client tools for Loopstack — McpModule.forRoot(), McpCallTool (mcp_call), McpListToolsTool (mcp_list_tools), McpToolConfig with allowedHosts, hostHeaderEnv, SSRF allowlist, Streamable HTTP and SSE transports, McpClientService, error hierarchy, McpMetricsPort
4
+ ---
16
5
 
17
- | Tool | Purpose |
18
- | ------------------ | ----------------------------------------------- |
19
- | `McpListToolsTool` | Discover the tools a remote MCP server exposes. |
20
- | `McpCallTool` | Invoke a tool on a remote MCP server. |
6
+ # @loopstack/mcp-module
21
7
 
22
- Both take `serverUrl`, `transport` (`streamableHttp` or `sse`), and `timeoutMs`
23
- at call time. `McpCallTool` additionally takes `toolName` and `arguments`.
8
+ > Remote-MCP client module for the [Loopstack](https://loopstack.ai) automation framework.
24
9
 
25
- ## Security model
10
+ Lets a Loopstack agent list and call tools on remote Model Context Protocol (MCP) servers over HTTPS — Streamable HTTP or legacy SSE — with a strict SSRF allowlist and zero-trust handling of authentication secrets.
26
11
 
27
- Every connection passes three checks before any bytes go out:
12
+ ## When to Use
28
13
 
29
- 1. **Allowlist** `serverUrl`'s hostname must match `allowedHosts`. Exact match
30
- or `*.example.com` (which also matches `example.com`).
31
- 2. **Scheme** — `https://` by default; `http://` only if `allowInsecureHttp: true`.
32
- 3. **Public-IP resolution** — DNS (or a literal IP) must resolve to a routable
33
- public address. Loopback, RFC1918, link-local, ULA, and IPv4-mapped
34
- equivalents are rejected. Override with `allowPrivateHosts: true` for trusted
35
- local MCP proxies.
14
+ - **Connect an agent to hosted MCP servers** (Linear, GitHub, internal tools) without writing custom tool wrappers for each API.
15
+ - **Dynamically discover remote tool schemas** at runtime via `mcp_list_tools`, then invoke them with `mcp_call`.
16
+ - **Reach multiple MCP servers from one workflow** — `serverUrl` is a per-call argument, so a single agent can hop between any allowlisted host.
17
+ - **Not an MCP server** — this module does not expose your workflows over MCP. It is a client only.
36
18
 
37
- Userinfo in the URL (`https://user:pw@host/...`) is rejected — credentials must
38
- flow through headers.
19
+ ## Installation
39
20
 
40
- ## Authentication
21
+ ```sh
22
+ npm install @loopstack/mcp-module
23
+ ```
41
24
 
42
- Configure auth headers via constructor injection config:
25
+ Register the module with `McpModule.forRoot()`:
43
26
 
44
27
  ```ts
45
- constructor(private readonly mcpCallTool: McpCallTool) {}
28
+ import { Module } from '@nestjs/common';
29
+ import { McpModule } from '@loopstack/mcp-module';
46
30
 
47
- // Configure allowedHosts and hostHeaderEnv via call config:
48
- await this.mcpCallTool.call(args, {
49
- config: {
50
- allowedHosts: ['mcp.linear.app'],
51
- hostHeaderEnv: { 'mcp.linear.app': { Authorization: 'LINEAR_MCP_TOKEN' } },
52
- },
53
- });
31
+ @Module({
32
+ imports: [
33
+ McpModule.forRoot({
34
+ allowedHosts: ['mcp.linear.app'],
35
+ hostHeaderEnv: {
36
+ 'mcp.linear.app': { Authorization: 'LINEAR_MCP_TOKEN' },
37
+ },
38
+ }),
39
+ ],
40
+ })
41
+ export class AppModule {}
54
42
  ```
55
43
 
56
- Three knobs, in increasing specificity:
57
-
58
- | Knob | Use for |
59
- | ---------------- | ----------------------------------------------------------------------------------------------------------------------- |
60
- | `defaultHeaders` | Static, **non-secret** values (e.g. `X-Trace: on`). Sensitive header names like `Authorization` are rejected here. |
61
- | `headerEnv` | `header → env-var` mapping applied to _every_ host. Value is read from `process.env` at call time. |
62
- | `hostHeaderEnv` | `host → { header → env-var }`. Use the hostname or `'*'` (applied to all). Host-specific entries override the wildcard. |
63
-
64
- Precedence (later wins): `defaultHeaders` → `headerEnv` → `hostHeaderEnv['*']` → `hostHeaderEnv[hostname]`. `hostHeaderEnv['*']` outranks `headerEnv` because it lives in the same map as the host-specific entries — keeping all host-scoped knobs together in `hostHeaderEnv` is the intended override layer. Keys are matched case-insensitively (HTTP semantics).
65
-
66
- Header _names_ are logged on connect (e.g. `headers=[Authorization]`); values
67
- never are. If a referenced env var is unset or empty, the header is silently
68
- omitted — so missing `LINEAR_MCP_TOKEN` means no `Authorization` header, not a
69
- crash.
70
-
71
- The header value is sent **raw**. If the remote server expects `Bearer <token>`,
72
- your env var must contain the `Bearer ` prefix:
44
+ Set the corresponding env var. The value is sent raw — include `Bearer ` if the server expects it:
73
45
 
74
46
  ```env
75
47
  LINEAR_MCP_TOKEN="Bearer lin_oauth_..."
76
48
  ```
77
49
 
78
- ## Registering the tools (workspace vs workflow)
79
-
80
- Import `McpModule` in your Nest module so the tool classes are available. Then
81
- register **instances** via the constructor — where you inject them
82
- depends on how you run the LLM loop.
83
-
84
- | How you run the agent | Where to injection MCP tools |
85
- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
86
- | **`ChatAgentWorkflow` / `AgentWorkflow` as a sub-workflow** (`this.agent.run({ tools: [...] })`) | **Workspace** — the child agent resolves tools from the executing workflow first, then the workspace. Tools on the parent workflow are not visible while the sub-agent runs. |
87
- | **Inline agent loop in one workflow** (your transitions call `this.llmGenerateText.call()` / `this.llmDelegateToolCalls.call()` on the same class) | **That workflow** — same pattern as other registry agents (e.g. Google Workspace). |
50
+ ## Quick Start
88
51
 
89
- See [@loopstack/agent Tool Resolution]() for the full resolution order.
52
+ This example mirrors the `@loopstack/mcp-linear-example-workflow` package. It starts a `ChatAgentWorkflow` sub-workflow with both MCP tools available.
90
53
 
91
- ### With `ChatAgentWorkflow` (register on the workspace)
54
+ ```ts
55
+ // mcp-linear.module.ts
56
+ import { Module } from '@nestjs/common';
57
+ import { AgentModule } from '@loopstack/agent';
58
+ import { McpModule } from '@loopstack/mcp-module';
59
+ import { McpLinearWorkflow } from './mcp-linear.workflow';
92
60
 
93
- This is the pattern used by `@loopstack/mcp-linear-example-workflow` and the app
94
- template: declare MCP tools once on the workspace, then pass their property names
95
- to `agent.run()`.
61
+ @Module({
62
+ imports: [
63
+ McpModule.forRoot({
64
+ allowedHosts: ['mcp.linear.app'],
65
+ hostHeaderEnv: { 'mcp.linear.app': { Authorization: 'LINEAR_MCP_TOKEN' } },
66
+ }),
67
+ AgentModule,
68
+ ],
69
+ providers: [McpLinearWorkflow],
70
+ exports: [McpLinearWorkflow],
71
+ })
72
+ export class McpLinearModule {}
73
+ ```
96
74
 
97
75
  ```ts
98
- import { Injectable } from '@nestjs/common';
76
+ // mcp-linear.workflow.ts
77
+ import { z } from 'zod';
99
78
  import { ChatAgentWorkflow } from '@loopstack/agent';
100
- import { InjectTool, InjectWorkflow, Workspace } from '@loopstack/common';
79
+ import { BaseWorkflow, LinkDocument, MessageDocument, Transition, Workflow } from '@loopstack/common';
80
+ import type { RunContext } from '@loopstack/common';
101
81
  import { McpCallTool, McpListToolsTool } from '@loopstack/mcp-module';
102
- import { MyMcpWorkflow } from './my-mcp.workflow';
103
82
 
104
- const mcpToolConfig = {
105
- allowedHosts: ['mcp.linear.app', 'mcp.github.com'],
106
- hostHeaderEnv: {
107
- 'mcp.linear.app': { Authorization: 'LINEAR_MCP_TOKEN' },
108
- 'mcp.github.com': { Authorization: 'GITHUB_MCP_TOKEN' },
109
- },
110
- } as const;
83
+ const ArgsSchema = z.object({
84
+ initialMessage: z.string().optional().default('List available Linear tools, then fetch my top 5 issues.'),
85
+ });
111
86
 
112
- @Injectable()
113
- @Workspace({ uiConfig: { title: 'My Workspace' } })
114
- export class MyWorkspace {
87
+ @Workflow({
88
+ title: 'MCP Linear',
89
+ description: 'Chat with an agent connected to Linear via MCP.',
90
+ schema: ArgsSchema,
91
+ })
92
+ export class McpLinearWorkflow extends BaseWorkflow<z.infer<typeof ArgsSchema>> {
115
93
  constructor(
116
- public readonly mcpAgent: MyMcpWorkflow,
117
- public readonly mcpListTools: McpListToolsTool,
118
- public readonly mcpCallTool: McpCallTool,
119
- ) {}
94
+ private readonly chatAgentWorkflow: ChatAgentWorkflow,
95
+ private readonly mcpListTools: McpListToolsTool,
96
+ private readonly mcpCallTool: McpCallTool,
97
+ ) {
98
+ super();
99
+ }
100
+
101
+ @Transition({ to: 'chatting' })
102
+ async startChat(state: Record<string, unknown>, ctx: RunContext): Promise<Record<string, unknown>> {
103
+ const args = ctx.args as z.infer<typeof ArgsSchema>;
104
+
105
+ const result = await this.chatAgentWorkflow.run({
106
+ system: 'You are a Linear assistant. Use mcp_list_tools to discover tools, then mcp_call to invoke them.',
107
+ tools: ['mcp_list_tools', 'mcp_call'],
108
+ userMessage: args.initialMessage,
109
+ });
110
+
111
+ await this.documentStore.save(LinkDocument, {
112
+ workflowId: result.workflowId,
113
+ label: 'Linear Agent Chat',
114
+ status: 'pending',
115
+ embed: true,
116
+ expanded: true,
117
+ });
118
+
119
+ return state;
120
+ }
120
121
  }
121
122
  ```
122
123
 
123
- ```ts
124
- // my-mcp.workflow.ts — parent only starts the sub-agent
125
- constructor(private readonly agent: ChatAgentWorkflow) { super(); }
124
+ Tools are referenced by their `@Tool({ name })` values (`'mcp_list_tools'`, `'mcp_call'`) and resolved from the NestJS DI container at runtime.
126
125
 
127
- await this.agent.run({
128
- system: '...',
129
- tools: ['mcpListTools', 'mcpCallTool'],
130
- userMessage: '...',
131
- });
126
+ ## How It Works
127
+
128
+ ```
129
+ Agent LLM loop
130
+
131
+ ├─ mcp_list_tools(serverUrl, transport?)
132
+ │ └─ McpClientService.listTools() → connect, list, disconnect
133
+
134
+ └─ mcp_call(serverUrl, toolName, arguments, transport?)
135
+ └─ McpClientService.callTool() → connect, call, disconnect
132
136
  ```
133
137
 
134
- ### Inline agent loop (register on the workflow)
138
+ Each tool call creates a fresh MCP client connection. Before any bytes go out, the URL passes through the security pipeline (allowlist, scheme, DNS resolution). Headers are merged from config and env vars are resolved at call time.
135
139
 
136
- If your workflow owns the LLM turns and tool delegation (no `ChatAgentWorkflow`
137
- sub-workflow), inject MCP tools on that same workflow class alongside
138
- `LlmGenerateTextTool` and `LlmDelegateToolCallsTool`.
140
+ The agent decides which `serverUrl` and `toolName` to use per call, so a single workflow can reach any host in `allowedHosts`.
139
141
 
140
- The agent picks `serverUrl` per call, so it can hop between any of the
141
- allowlisted hosts within the same chat.
142
+ ### Security Model
142
143
 
143
- ## Multiple MCP servers
144
+ Every connection passes three checks:
144
145
 
145
- `serverUrl` is a per-call argument. **An agent can reach any host listed in
146
- `allowedHosts`**there is no "primary" server. To add a new server:
146
+ 1. **Allowlist** — `serverUrl`'s hostname must match `allowedHosts`. Exact match or `*.example.com` (which also matches `example.com`).
147
+ 2. **Scheme** `https://` by default; `http://` only if `allowInsecureHttp: true`.
148
+ 3. **Public-IP resolution** — DNS (or a literal IP) must resolve to a routable public address. Loopback, RFC1918, link-local, ULA, and IPv4-mapped equivalents are rejected. Override with `allowPrivateHosts: true` for trusted local MCP proxies.
147
149
 
148
- 1. Add its hostname to `allowedHosts` on both injection configs.
149
- 2. Add its auth mapping to `hostHeaderEnv`.
150
- 3. Set the corresponding env var.
150
+ Userinfo in the URL (`https://user:pw@host/...`) is rejected credentials must flow through headers.
151
+
152
+ ### Authentication
153
+
154
+ Three knobs, in increasing specificity:
155
+
156
+ | Knob | Use for |
157
+ | ---------------- | ------------------------------------------------------------------------------------------------------------------ |
158
+ | `defaultHeaders` | Static, **non-secret** values (e.g. `X-Trace: on`). Sensitive header names like `Authorization` are rejected here. |
159
+ | `headerEnv` | `header -> env-var` mapping applied to every host. Value is read from `process.env` at call time. |
160
+ | `hostHeaderEnv` | `host -> { header -> env-var }`. Use `'*'` for all hosts. Host-specific entries override the wildcard. |
151
161
 
152
- Dynamic, end-user-driven server registration (paste any URL mid-chat) is
153
- deliberately _not_ supported — the allowlist exists to prevent agents from
154
- being tricked into hitting internal hosts. Adding that flow safely requires
155
- a per-user registry plus an auth onboarding step (out of scope here).
162
+ Precedence (later wins): `defaultHeaders` -> `headerEnv` -> `hostHeaderEnv['*']` -> `hostHeaderEnv[hostname]`.
156
163
 
157
- ## Transport guidance
164
+ Header names are logged on connect (e.g. `headers=[Authorization]`); values never are. If a referenced env var is unset or empty, the header is silently omitted.
165
+
166
+ ### Transport
158
167
 
159
168
  | Server | Transport |
160
169
  | ------------------------- | -------------------------- |
@@ -163,21 +172,107 @@ a per-user registry plus an auth onboarding step (out of scope here).
163
172
 
164
173
  Pass `transport: 'sse'` per call when needed.
165
174
 
175
+ ### Multiple MCP Servers
176
+
177
+ `serverUrl` is a per-call argument. An agent can reach any host listed in `allowedHosts`. To add a new server:
178
+
179
+ 1. Add its hostname to `allowedHosts`.
180
+ 2. Add its auth mapping to `hostHeaderEnv`.
181
+ 3. Set the corresponding env var.
182
+
183
+ Dynamic, end-user-driven server registration (paste any URL mid-chat) is deliberately not supported — the allowlist exists to prevent agents from being tricked into hitting internal hosts.
184
+
185
+ ## Tools Reference
186
+
187
+ ### `mcp_list_tools`
188
+
189
+ Lists tool definitions exposed by a remote MCP server.
190
+
191
+ **Class:** `McpListToolsTool`
192
+
193
+ | Arg | Type | Required | Description |
194
+ | ----------- | --------------------------- | -------- | ----------------------------------------------------------- |
195
+ | `serverUrl` | `z.url()` | Yes | MCP endpoint URL (https recommended). |
196
+ | `transport` | `'streamableHttp' \| 'sse'` | No | Default `'streamableHttp'`. Use `'sse'` for legacy servers. |
197
+ | `timeoutMs` | `number` (1–900000) | No | Per-request timeout in milliseconds. |
198
+
199
+ **Config:** `McpToolConfig` (via `configSchema` / `options.config` / `McpModule.forRoot()`)
200
+
201
+ **Returns:** `{ data: { tools: unknown } }` — the MCP `tools/list` response.
202
+
203
+ ### `mcp_call`
204
+
205
+ Calls a tool on a remote MCP server.
206
+
207
+ **Class:** `McpCallTool`
208
+
209
+ | Arg | Type | Required | Description |
210
+ | ----------- | --------------------------- | -------- | ----------------------------------------------------------- |
211
+ | `serverUrl` | `z.url()` | Yes | MCP endpoint URL (https recommended). |
212
+ | `toolName` | `string` | Yes | Name of the remote MCP tool to invoke. |
213
+ | `arguments` | `Record<string, unknown>` | No | JSON object passed to the tool. Defaults to `{}`. |
214
+ | `transport` | `'streamableHttp' \| 'sse'` | No | Default `'streamableHttp'`. Use `'sse'` for legacy servers. |
215
+ | `timeoutMs` | `number` (1–900000) | No | Per-request timeout in milliseconds. |
216
+
217
+ **Config:** `McpToolConfig` (via `configSchema` / `options.config` / `McpModule.forRoot()`)
218
+
219
+ **Returns:** `{ data: McpCallToolResult }` — either `{ kind: 'callToolResult', content, structuredContent?, isError? }` or `{ kind: 'legacyToolResult', toolResult }`.
220
+
221
+ ## Configuration
222
+
223
+ ### `McpModule.forRoot(config?)`
224
+
225
+ Registers the module globally. Config is optional — if omitted, each tool call must provide config via `options.config`.
226
+
227
+ | Option | Type | Default | Description |
228
+ | ------------------- | ---------------------------------------- | ------- | ------------------------------------------------------------------------------------------ |
229
+ | `allowedHosts` | `string[]` | — | Required. Hostnames allowed for `serverUrl`. Use `*.example.com` for wildcard. |
230
+ | `allowInsecureHttp` | `boolean` | `false` | Allow `http://` URLs. |
231
+ | `allowPrivateHosts` | `boolean` | `false` | Skip public-IP DNS check. For trusted local MCP proxies only. |
232
+ | `defaultHeaders` | `Record<string, string>` | — | Static non-secret headers. Sensitive names (`Authorization`, `Cookie`, etc.) are rejected. |
233
+ | `headerEnv` | `Record<string, string>` | — | `headerName -> envVarName` applied to every host. |
234
+ | `hostHeaderEnv` | `Record<string, Record<string, string>>` | — | `hostname -> { headerName -> envVarName }`. Use `'*'` for all hosts. |
235
+
236
+ ### Per-call config override
237
+
238
+ Inject the tool and pass config via `options.config`:
239
+
240
+ ```ts
241
+ await this.mcpCallTool.call(args, {
242
+ config: {
243
+ allowedHosts: ['mcp.linear.app'],
244
+ hostHeaderEnv: { 'mcp.linear.app': { Authorization: 'LINEAR_MCP_TOKEN' } },
245
+ },
246
+ });
247
+ ```
248
+
249
+ Per-call config overrides `McpModule.forRoot()` defaults entirely. If no config is provided at either level, the tool throws.
250
+
251
+ ### Provider tokens
252
+
253
+ | Token | Default | Description |
254
+ | -------------------- | ------------------ | ---------------------------------------------------------------------------------------- |
255
+ | `MCP_METRICS` | `NoopMcpMetrics` | Implement `McpMetricsPort` for OpenTelemetry or custom metrics. |
256
+ | `MCP_ENV_READER` | `ProcessEnvReader` | Implement `EnvReader` to source secrets from a secrets manager instead of `process.env`. |
257
+ | `MCP_DEFAULT_CONFIG` | `null` | Set by `McpModule.forRoot()`. Can also be provided manually. |
258
+
166
259
  ## Errors
167
260
 
168
261
  All failures throw subclasses of `McpError`:
169
262
 
170
- - `McpUrlSecurityError` SSRF / allowlist / scheme / userinfo violations.
171
- - `McpAuthError` 401 / 403 from the remote (or transport-equivalent).
172
- - `McpTimeoutError` call exceeded `timeoutMs`.
173
- - `McpProtocolError` malformed MCP response.
174
- - `McpTransportError` DNS, TCP, TLS, abort, fallback.
263
+ | Error class | Cause |
264
+ | --------------------- | ------------------------------------------------ |
265
+ | `McpUrlSecurityError` | SSRF / allowlist / scheme / userinfo violations. |
266
+ | `McpAuthError` | 401 / 403 from the remote server. |
267
+ | `McpTimeoutError` | Call exceeded `timeoutMs`. |
268
+ | `McpProtocolError` | Malformed MCP response (JSON-RPC parse/invalid). |
269
+ | `McpTransportError` | DNS, TCP, TLS, abort, or fallback failures. |
175
270
 
176
271
  Catch `McpError` for any failure, or a specific subclass to react to a category.
177
272
 
178
273
  ## Observability
179
274
 
180
- The service logs structured events with header _names_ only, never values:
275
+ The service logs structured events with header names only, never values:
181
276
 
182
277
  ```
183
278
  mcp.connect host=mcp.linear.app transport=sse headers=[Authorization]
@@ -185,8 +280,7 @@ mcp.connect.done host=mcp.linear.app transport=sse outcome=success latencyMs=412
185
280
  mcp.callTool host=mcp.linear.app transport=sse toolName=createIssue outcome=success latencyMs=623
186
281
  ```
187
282
 
188
- For metrics, implement `McpMetricsPort` and bind it via the `MCP_METRICS`
189
- provider token — the default is a no-op:
283
+ Override `MCP_METRICS` for custom metric collection:
190
284
 
191
285
  ```ts
192
286
  @Module({
@@ -194,20 +288,37 @@ provider token — the default is a no-op:
194
288
  })
195
289
  ```
196
290
 
197
- Same pattern for `MCP_ENV_READER` if you need to source secrets from somewhere
198
- other than `process.env` (e.g. a secrets manager).
291
+ ## Public API
199
292
 
200
- ## Testing
293
+ - **Module:** `McpModule`
294
+ - **Tools:** `McpCallTool`, `McpListToolsTool`, `McpToolBase`
295
+ - **Services:** `McpClientService`
296
+ - **Schemas:** `McpToolConfigSchema`, `McpCallToolArgsSchema`, `McpListToolsArgsSchema`, `McpConnectionArgsSchema`
297
+ - **Types:** `McpToolConfig`, `McpToolConfigInput`, `McpTransportKind`, `McpCallToolResult`, `McpClientCallOptions`
298
+ - **Errors:** `McpError`, `McpUrlSecurityError`, `McpAuthError`, `McpTimeoutError`, `McpProtocolError`, `McpTransportError`
299
+ - **Tokens:** `MCP_DEFAULT_CONFIG`, `MCP_METRICS`, `MCP_ENV_READER`
300
+ - **Interfaces:** `McpMetricsPort`, `McpConnectSample`, `McpCallSample`, `McpCallOutcome`, `EnvReader`
301
+ - **Utilities:** `hostMatchesAllowlist`, `assertIpIsPublic`, `assertResolvableHostIsPublic`, `assertMcpUrlSafe`
302
+ - **Implementations:** `ProcessEnvReader`, `NoopMcpMetrics`
201
303
 
202
- The module ships unit tests for:
304
+ ## Dependencies
203
305
 
204
- - `hostMatchesAllowlist`, `assertIpIsPublic`, `assertMcpUrlSafe` (with mocked DNS)
205
- - `McpClientService.mergeHeaders` (precedence, host scoping, env-var skipping)
206
- - Both tools (config guard + arg forwarding)
207
- - Config-schema validation (secret-header rejection, strict mode, required keys)
306
+ | Package | Role |
307
+ | -------------------------------- | ---------------------------------------------------- |
308
+ | `@modelcontextprotocol/sdk` | MCP client, Streamable HTTP and SSE transports |
309
+ | `@loopstack/common` | `BaseTool`, `@Tool`, `ToolCallOptions`, `RunContext` |
310
+ | `@loopstack/core` | `LoopCoreModule` (NestJS integration) |
311
+ | `@nestjs/common`, `@nestjs/core` | Dependency injection, module system |
312
+ | `zod` | Schema validation |
208
313
 
209
- Run with:
314
+ ## Related
210
315
 
211
- ```sh
212
- npm test
213
- ```
316
+ - [`@loopstack/mcp-linear-example-workflow`](https://loopstack.ai/docs/registry/examples/mcp-linear-example-workflow) — Full working example connecting a ChatAgentWorkflow to Linear via MCP.
317
+ - [Agent Workflows](https://loopstack.ai/docs/build/ai/agent-workflows) — How `ChatAgentWorkflow` and tool resolution work.
318
+ - [Tool Configuration](https://loopstack.ai/docs/build/fundamentals/tools) — How `configSchema` and `options.config` are merged at call time.
319
+ - [`@loopstack/claude-module`](https://loopstack.ai/docs/registry/features/claude-module) — LLM provider that powers the agent loop calling MCP tools.
320
+
321
+ ## About
322
+
323
+ **Author:** Jakob Klippel
324
+ **License:** MIT
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { ToolCallOptions, ToolResult } from '@loopstack/common';
3
- import type { LoopstackContext } from '@loopstack/common';
3
+ import type { RunContext } from '@loopstack/common';
4
4
  import type { McpToolConfig } from '../config/mcp-tool-config.schema.js';
5
5
  import { McpToolBase } from './mcp-tool-base.js';
6
6
  export declare const McpCallToolArgsSchema: z.ZodObject<{
@@ -15,6 +15,6 @@ export declare const McpCallToolArgsSchema: z.ZodObject<{
15
15
  }, z.core.$strict>;
16
16
  export type McpCallToolArgs = z.infer<typeof McpCallToolArgsSchema>;
17
17
  export declare class McpCallTool extends McpToolBase<McpCallToolArgs> {
18
- protected handle(args: McpCallToolArgs, ctx: LoopstackContext, options?: ToolCallOptions<McpToolConfig>): Promise<ToolResult>;
18
+ protected handle(args: McpCallToolArgs, ctx: RunContext, options?: ToolCallOptions<McpToolConfig>): Promise<ToolResult>;
19
19
  }
20
20
  //# sourceMappingURL=mcp-call.tool.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-call.tool.d.ts","sourceRoot":"","sources":["../../src/tools/mcp-call.tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAQ,eAAe,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAC;AAEzE,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,eAAO,MAAM,qBAAqB;;;;;;;;;kBAGvB,CAAC;AAEZ,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,qBAOa,WAAY,SAAQ,WAAW,CAAC,eAAe,CAAC;cAC3C,MAAM,CACpB,IAAI,EAAE,eAAe,EACrB,GAAG,EAAE,gBAAgB,EACrB,OAAO,CAAC,EAAE,eAAe,CAAC,aAAa,CAAC,GACvC,OAAO,CAAC,UAAU,CAAC;CAUvB"}
1
+ {"version":3,"file":"mcp-call.tool.d.ts","sourceRoot":"","sources":["../../src/tools/mcp-call.tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAQ,eAAe,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACtE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAC;AAEzE,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,eAAO,MAAM,qBAAqB;;;;;;;;;kBAGvB,CAAC;AAEZ,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,qBAOa,WAAY,SAAQ,WAAW,CAAC,eAAe,CAAC;cAC3C,MAAM,CACpB,IAAI,EAAE,eAAe,EACrB,GAAG,EAAE,UAAU,EACf,OAAO,CAAC,EAAE,eAAe,CAAC,aAAa,CAAC,GACvC,OAAO,CAAC,UAAU,CAAC;CAUvB"}
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-call.tool.js","sourceRoot":"","sources":["../../src/tools/mcp-call.tool.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,IAAI,EAA+B,MAAM,mBAAmB,CAAC;AAEtE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAE1E,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAC1E,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,MAAM,CAAC,MAAM,qBAAqB,GAAG,uBAAuB,CAAC,MAAM,CAAC;IAClE,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,wCAAwC,CAAC;IAC9E,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,iCAAiC,CAAC;CAChH,CAAC,CAAC,MAAM,EAAE,CAAC;AAWL,IAAM,WAAW,GAAjB,MAAM,WAAY,SAAQ,WAA4B;IACjD,KAAK,CAAC,MAAM,CACpB,IAAqB,EACrB,GAAqB,EACrB,OAAwC;QAExC,MAAM,GAAG,GAAG,IAAI,CAAC,aAAa,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAEhD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE;YACzF,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,SAAS,EAAE,IAAI,CAAC,SAAS;SAC1B,CAAC,CAAC;QAEH,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAC1B,CAAC;CACF,CAAA;AAfY,WAAW;IAPvB,IAAI,CAAC;QACJ,IAAI,EAAE,UAAU;QAChB,WAAW,EACT,mKAAmK;QACrK,MAAM,EAAE,qBAAqB;QAC7B,YAAY,EAAE,mBAAmB;KAClC,CAAC;GACW,WAAW,CAevB"}
1
+ {"version":3,"file":"mcp-call.tool.js","sourceRoot":"","sources":["../../src/tools/mcp-call.tool.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,IAAI,EAA+B,MAAM,mBAAmB,CAAC;AAEtE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAE1E,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAC1E,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,MAAM,CAAC,MAAM,qBAAqB,GAAG,uBAAuB,CAAC,MAAM,CAAC;IAClE,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,wCAAwC,CAAC;IAC9E,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,iCAAiC,CAAC;CAChH,CAAC,CAAC,MAAM,EAAE,CAAC;AAWL,IAAM,WAAW,GAAjB,MAAM,WAAY,SAAQ,WAA4B;IACjD,KAAK,CAAC,MAAM,CACpB,IAAqB,EACrB,GAAe,EACf,OAAwC;QAExC,MAAM,GAAG,GAAG,IAAI,CAAC,aAAa,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAEhD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,EAAE;YACzF,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,SAAS,EAAE,IAAI,CAAC,SAAS;SAC1B,CAAC,CAAC;QAEH,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAC1B,CAAC;CACF,CAAA;AAfY,WAAW;IAPvB,IAAI,CAAC;QACJ,IAAI,EAAE,UAAU;QAChB,WAAW,EACT,mKAAmK;QACrK,MAAM,EAAE,qBAAqB;QAC7B,YAAY,EAAE,mBAAmB;KAClC,CAAC;GACW,WAAW,CAevB"}
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { ToolCallOptions, ToolResult } from '@loopstack/common';
3
- import type { LoopstackContext } from '@loopstack/common';
3
+ import type { RunContext } from '@loopstack/common';
4
4
  import type { McpToolConfig } from '../config/mcp-tool-config.schema.js';
5
5
  import { McpToolBase } from './mcp-tool-base.js';
6
6
  export declare const McpListToolsArgsSchema: z.ZodObject<{
@@ -13,6 +13,6 @@ export declare const McpListToolsArgsSchema: z.ZodObject<{
13
13
  }, z.core.$strict>;
14
14
  export type McpListToolsArgs = z.infer<typeof McpListToolsArgsSchema>;
15
15
  export declare class McpListToolsTool extends McpToolBase<McpListToolsArgs> {
16
- protected handle(args: McpListToolsArgs, ctx: LoopstackContext, options?: ToolCallOptions<McpToolConfig>): Promise<ToolResult>;
16
+ protected handle(args: McpListToolsArgs, ctx: RunContext, options?: ToolCallOptions<McpToolConfig>): Promise<ToolResult>;
17
17
  }
18
18
  //# sourceMappingURL=mcp-list-tools.tool.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-list-tools.tool.d.ts","sourceRoot":"","sources":["../../src/tools/mcp-list-tools.tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAQ,eAAe,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAC;AAEzE,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,eAAO,MAAM,sBAAsB;;;;;;;kBAA0B,CAAC;AAE9D,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,qBAOa,gBAAiB,SAAQ,WAAW,CAAC,gBAAgB,CAAC;cACjD,MAAM,CACpB,IAAI,EAAE,gBAAgB,EACtB,GAAG,EAAE,gBAAgB,EACrB,OAAO,CAAC,EAAE,eAAe,CAAC,aAAa,CAAC,GACvC,OAAO,CAAC,UAAU,CAAC;CAUvB"}
1
+ {"version":3,"file":"mcp-list-tools.tool.d.ts","sourceRoot":"","sources":["../../src/tools/mcp-list-tools.tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAQ,eAAe,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACtE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qCAAqC,CAAC;AAEzE,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,eAAO,MAAM,sBAAsB;;;;;;;kBAA0B,CAAC;AAE9D,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,qBAOa,gBAAiB,SAAQ,WAAW,CAAC,gBAAgB,CAAC;cACjD,MAAM,CACpB,IAAI,EAAE,gBAAgB,EACtB,GAAG,EAAE,UAAU,EACf,OAAO,CAAC,EAAE,eAAe,CAAC,aAAa,CAAC,GACvC,OAAO,CAAC,UAAU,CAAC;CAUvB"}
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-list-tools.tool.js","sourceRoot":"","sources":["../../src/tools/mcp-list-tools.tool.ts"],"names":[],"mappings":";;;;;;AACA,OAAO,EAAE,IAAI,EAA+B,MAAM,mBAAmB,CAAC;AAEtE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAE1E,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAC1E,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,MAAM,CAAC,MAAM,sBAAsB,GAAG,uBAAuB,CAAC;AAWvD,IAAM,gBAAgB,GAAtB,MAAM,gBAAiB,SAAQ,WAA6B;IACvD,KAAK,CAAC,MAAM,CACpB,IAAsB,EACtB,GAAqB,EACrB,OAAwC;QAExC,MAAM,GAAG,GAAG,IAAI,CAAC,aAAa,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAEhD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE;YAC3D,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,SAAS,EAAE,IAAI,CAAC,SAAS;SAC1B,CAAC,CAAC;QAEH,OAAO,EAAE,IAAI,EAAE,MAAiC,EAAE,CAAC;IACrD,CAAC;CACF,CAAA;AAfY,gBAAgB;IAP5B,IAAI,CAAC;QACJ,IAAI,EAAE,gBAAgB;QACtB,WAAW,EACT,6JAA6J;QAC/J,MAAM,EAAE,sBAAsB;QAC9B,YAAY,EAAE,mBAAmB;KAClC,CAAC;GACW,gBAAgB,CAe5B"}
1
+ {"version":3,"file":"mcp-list-tools.tool.js","sourceRoot":"","sources":["../../src/tools/mcp-list-tools.tool.ts"],"names":[],"mappings":";;;;;;AACA,OAAO,EAAE,IAAI,EAA+B,MAAM,mBAAmB,CAAC;AAEtE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAE1E,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAC1E,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,MAAM,CAAC,MAAM,sBAAsB,GAAG,uBAAuB,CAAC;AAWvD,IAAM,gBAAgB,GAAtB,MAAM,gBAAiB,SAAQ,WAA6B;IACvD,KAAK,CAAC,MAAM,CACpB,IAAsB,EACtB,GAAe,EACf,OAAwC;QAExC,MAAM,GAAG,GAAG,IAAI,CAAC,aAAa,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAEhD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE;YAC3D,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,SAAS,EAAE,IAAI,CAAC,SAAS;SAC1B,CAAC,CAAC;QAEH,OAAO,EAAE,IAAI,EAAE,MAAiC,EAAE,CAAC;IACrD,CAAC;CACF,CAAA;AAfY,gBAAgB;IAP5B,IAAI,CAAC;QACJ,IAAI,EAAE,gBAAgB;QACtB,WAAW,EACT,6JAA6J;QAC/J,MAAM,EAAE,sBAAsB;QAC9B,YAAY,EAAE,mBAAmB;KAClC,CAAC;GACW,gBAAgB,CAe5B"}
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "loopstack",
9
9
  "tool"
10
10
  ],
11
- "version": "0.3.0",
11
+ "version": "0.3.2",
12
12
  "license": "MIT",
13
13
  "author": {
14
14
  "name": "Jakob Klippel",
@@ -32,19 +32,24 @@
32
32
  "watch": "nest build --watch"
33
33
  },
34
34
  "dependencies": {
35
- "@loopstack/common": "^0.32.0",
36
- "@loopstack/core": "^0.32.0",
37
- "@modelcontextprotocol/sdk": "^1.29.0",
35
+ "@loopstack/common": "^0.33.0",
36
+ "@loopstack/core": "^0.33.0",
37
+ "@modelcontextprotocol/sdk": "^1.29.0"
38
+ },
39
+ "devDependencies": {
38
40
  "@nestjs/common": "^11.1.19",
39
41
  "@nestjs/core": "^11.1.19",
42
+ "@swc/core": "^1.15.33",
43
+ "unplugin-swc": "^1.5.9",
44
+ "vitest": "^4.1.6",
40
45
  "zod": "^4.3.6"
41
46
  },
47
+ "peerDependencies": {
48
+ "@nestjs/common": "^11.0.0",
49
+ "@nestjs/core": "^11.0.0",
50
+ "zod": "^4.0.0"
51
+ },
42
52
  "files": [
43
53
  "dist"
44
- ],
45
- "devDependencies": {
46
- "vitest": "^4.1.6",
47
- "@swc/core": "^1.15.33",
48
- "unplugin-swc": "^1.5.9"
49
- }
54
+ ]
50
55
  }