@loopstack/mcp-module 0.3.1 → 0.3.3

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,164 @@
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, 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
+ await this.chatAgentWorkflow.run(
106
+ {
107
+ system: 'You are a Linear assistant. Use mcp_list_tools to discover tools, then mcp_call to invoke them.',
108
+ tools: ['mcp_list_tools', 'mcp_call'],
109
+ userMessage: args.initialMessage,
110
+ },
111
+ { show: 'inline', label: 'Linear Agent Chat' },
112
+ );
113
+
114
+ return state;
115
+ }
120
116
  }
121
117
  ```
122
118
 
123
- ```ts
124
- // my-mcp.workflow.ts — parent only starts the sub-agent
125
- constructor(private readonly agent: ChatAgentWorkflow) { super(); }
119
+ Tools are referenced by their `@Tool({ name })` values (`'mcp_list_tools'`, `'mcp_call'`) and resolved from the NestJS DI container at runtime.
126
120
 
127
- await this.agent.run({
128
- system: '...',
129
- tools: ['mcpListTools', 'mcpCallTool'],
130
- userMessage: '...',
131
- });
121
+ ## How It Works
122
+
123
+ ```
124
+ Agent LLM loop
125
+
126
+ ├─ mcp_list_tools(serverUrl, transport?)
127
+ │ └─ McpClientService.listTools() → connect, list, disconnect
128
+
129
+ └─ mcp_call(serverUrl, toolName, arguments, transport?)
130
+ └─ McpClientService.callTool() → connect, call, disconnect
132
131
  ```
133
132
 
134
- ### Inline agent loop (register on the workflow)
133
+ 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
134
 
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`.
135
+ The agent decides which `serverUrl` and `toolName` to use per call, so a single workflow can reach any host in `allowedHosts`.
139
136
 
140
- The agent picks `serverUrl` per call, so it can hop between any of the
141
- allowlisted hosts within the same chat.
137
+ ### Security Model
142
138
 
143
- ## Multiple MCP servers
139
+ Every connection passes three checks:
144
140
 
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:
141
+ 1. **Allowlist** — `serverUrl`'s hostname must match `allowedHosts`. Exact match or `*.example.com` (which also matches `example.com`).
142
+ 2. **Scheme** `https://` by default; `http://` only if `allowInsecureHttp: true`.
143
+ 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
144
 
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.
145
+ Userinfo in the URL (`https://user:pw@host/...`) is rejected credentials must flow through headers.
146
+
147
+ ### Authentication
148
+
149
+ Three knobs, in increasing specificity:
150
+
151
+ | Knob | Use for |
152
+ | ---------------- | ------------------------------------------------------------------------------------------------------------------ |
153
+ | `defaultHeaders` | Static, **non-secret** values (e.g. `X-Trace: on`). Sensitive header names like `Authorization` are rejected here. |
154
+ | `headerEnv` | `header -> env-var` mapping applied to every host. Value is read from `process.env` at call time. |
155
+ | `hostHeaderEnv` | `host -> { header -> env-var }`. Use `'*'` for all hosts. Host-specific entries override the wildcard. |
151
156
 
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).
157
+ Precedence (later wins): `defaultHeaders` -> `headerEnv` -> `hostHeaderEnv['*']` -> `hostHeaderEnv[hostname]`.
156
158
 
157
- ## Transport guidance
159
+ 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.
160
+
161
+ ### Transport
158
162
 
159
163
  | Server | Transport |
160
164
  | ------------------------- | -------------------------- |
@@ -163,21 +167,107 @@ a per-user registry plus an auth onboarding step (out of scope here).
163
167
 
164
168
  Pass `transport: 'sse'` per call when needed.
165
169
 
170
+ ### Multiple MCP Servers
171
+
172
+ `serverUrl` is a per-call argument. An agent can reach any host listed in `allowedHosts`. To add a new server:
173
+
174
+ 1. Add its hostname to `allowedHosts`.
175
+ 2. Add its auth mapping to `hostHeaderEnv`.
176
+ 3. Set the corresponding env var.
177
+
178
+ 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.
179
+
180
+ ## Tools Reference
181
+
182
+ ### `mcp_list_tools`
183
+
184
+ Lists tool definitions exposed by a remote MCP server.
185
+
186
+ **Class:** `McpListToolsTool`
187
+
188
+ | Arg | Type | Required | Description |
189
+ | ----------- | --------------------------- | -------- | ----------------------------------------------------------- |
190
+ | `serverUrl` | `z.url()` | Yes | MCP endpoint URL (https recommended). |
191
+ | `transport` | `'streamableHttp' \| 'sse'` | No | Default `'streamableHttp'`. Use `'sse'` for legacy servers. |
192
+ | `timeoutMs` | `number` (1–900000) | No | Per-request timeout in milliseconds. |
193
+
194
+ **Config:** `McpToolConfig` (via `configSchema` / `options.config` / `McpModule.forRoot()`)
195
+
196
+ **Returns:** `{ data: { tools: unknown } }` — the MCP `tools/list` response.
197
+
198
+ ### `mcp_call`
199
+
200
+ Calls a tool on a remote MCP server.
201
+
202
+ **Class:** `McpCallTool`
203
+
204
+ | Arg | Type | Required | Description |
205
+ | ----------- | --------------------------- | -------- | ----------------------------------------------------------- |
206
+ | `serverUrl` | `z.url()` | Yes | MCP endpoint URL (https recommended). |
207
+ | `toolName` | `string` | Yes | Name of the remote MCP tool to invoke. |
208
+ | `arguments` | `Record<string, unknown>` | No | JSON object passed to the tool. Defaults to `{}`. |
209
+ | `transport` | `'streamableHttp' \| 'sse'` | No | Default `'streamableHttp'`. Use `'sse'` for legacy servers. |
210
+ | `timeoutMs` | `number` (1–900000) | No | Per-request timeout in milliseconds. |
211
+
212
+ **Config:** `McpToolConfig` (via `configSchema` / `options.config` / `McpModule.forRoot()`)
213
+
214
+ **Returns:** `{ data: McpCallToolResult }` — either `{ kind: 'callToolResult', content, structuredContent?, isError? }` or `{ kind: 'legacyToolResult', toolResult }`.
215
+
216
+ ## Configuration
217
+
218
+ ### `McpModule.forRoot(config?)`
219
+
220
+ Registers the module globally. Config is optional — if omitted, each tool call must provide config via `options.config`.
221
+
222
+ | Option | Type | Default | Description |
223
+ | ------------------- | ---------------------------------------- | ------- | ------------------------------------------------------------------------------------------ |
224
+ | `allowedHosts` | `string[]` | — | Required. Hostnames allowed for `serverUrl`. Use `*.example.com` for wildcard. |
225
+ | `allowInsecureHttp` | `boolean` | `false` | Allow `http://` URLs. |
226
+ | `allowPrivateHosts` | `boolean` | `false` | Skip public-IP DNS check. For trusted local MCP proxies only. |
227
+ | `defaultHeaders` | `Record<string, string>` | — | Static non-secret headers. Sensitive names (`Authorization`, `Cookie`, etc.) are rejected. |
228
+ | `headerEnv` | `Record<string, string>` | — | `headerName -> envVarName` applied to every host. |
229
+ | `hostHeaderEnv` | `Record<string, Record<string, string>>` | — | `hostname -> { headerName -> envVarName }`. Use `'*'` for all hosts. |
230
+
231
+ ### Per-call config override
232
+
233
+ Inject the tool and pass config via `options.config`:
234
+
235
+ ```ts
236
+ await this.mcpCallTool.call(args, {
237
+ config: {
238
+ allowedHosts: ['mcp.linear.app'],
239
+ hostHeaderEnv: { 'mcp.linear.app': { Authorization: 'LINEAR_MCP_TOKEN' } },
240
+ },
241
+ });
242
+ ```
243
+
244
+ Per-call config overrides `McpModule.forRoot()` defaults entirely. If no config is provided at either level, the tool throws.
245
+
246
+ ### Provider tokens
247
+
248
+ | Token | Default | Description |
249
+ | -------------------- | ------------------ | ---------------------------------------------------------------------------------------- |
250
+ | `MCP_METRICS` | `NoopMcpMetrics` | Implement `McpMetricsPort` for OpenTelemetry or custom metrics. |
251
+ | `MCP_ENV_READER` | `ProcessEnvReader` | Implement `EnvReader` to source secrets from a secrets manager instead of `process.env`. |
252
+ | `MCP_DEFAULT_CONFIG` | `null` | Set by `McpModule.forRoot()`. Can also be provided manually. |
253
+
166
254
  ## Errors
167
255
 
168
256
  All failures throw subclasses of `McpError`:
169
257
 
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.
258
+ | Error class | Cause |
259
+ | --------------------- | ------------------------------------------------ |
260
+ | `McpUrlSecurityError` | SSRF / allowlist / scheme / userinfo violations. |
261
+ | `McpAuthError` | 401 / 403 from the remote server. |
262
+ | `McpTimeoutError` | Call exceeded `timeoutMs`. |
263
+ | `McpProtocolError` | Malformed MCP response (JSON-RPC parse/invalid). |
264
+ | `McpTransportError` | DNS, TCP, TLS, abort, or fallback failures. |
175
265
 
176
266
  Catch `McpError` for any failure, or a specific subclass to react to a category.
177
267
 
178
268
  ## Observability
179
269
 
180
- The service logs structured events with header _names_ only, never values:
270
+ The service logs structured events with header names only, never values:
181
271
 
182
272
  ```
183
273
  mcp.connect host=mcp.linear.app transport=sse headers=[Authorization]
@@ -185,8 +275,7 @@ mcp.connect.done host=mcp.linear.app transport=sse outcome=success latencyMs=412
185
275
  mcp.callTool host=mcp.linear.app transport=sse toolName=createIssue outcome=success latencyMs=623
186
276
  ```
187
277
 
188
- For metrics, implement `McpMetricsPort` and bind it via the `MCP_METRICS`
189
- provider token — the default is a no-op:
278
+ Override `MCP_METRICS` for custom metric collection:
190
279
 
191
280
  ```ts
192
281
  @Module({
@@ -194,20 +283,37 @@ provider token — the default is a no-op:
194
283
  })
195
284
  ```
196
285
 
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).
286
+ ## Public API
199
287
 
200
- ## Testing
288
+ - **Module:** `McpModule`
289
+ - **Tools:** `McpCallTool`, `McpListToolsTool`, `McpToolBase`
290
+ - **Services:** `McpClientService`
291
+ - **Schemas:** `McpToolConfigSchema`, `McpCallToolArgsSchema`, `McpListToolsArgsSchema`, `McpConnectionArgsSchema`
292
+ - **Types:** `McpToolConfig`, `McpToolConfigInput`, `McpTransportKind`, `McpCallToolResult`, `McpClientCallOptions`
293
+ - **Errors:** `McpError`, `McpUrlSecurityError`, `McpAuthError`, `McpTimeoutError`, `McpProtocolError`, `McpTransportError`
294
+ - **Tokens:** `MCP_DEFAULT_CONFIG`, `MCP_METRICS`, `MCP_ENV_READER`
295
+ - **Interfaces:** `McpMetricsPort`, `McpConnectSample`, `McpCallSample`, `McpCallOutcome`, `EnvReader`
296
+ - **Utilities:** `hostMatchesAllowlist`, `assertIpIsPublic`, `assertResolvableHostIsPublic`, `assertMcpUrlSafe`
297
+ - **Implementations:** `ProcessEnvReader`, `NoopMcpMetrics`
201
298
 
202
- The module ships unit tests for:
299
+ ## Dependencies
203
300
 
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)
301
+ | Package | Role |
302
+ | -------------------------------- | ---------------------------------------------------- |
303
+ | `@modelcontextprotocol/sdk` | MCP client, Streamable HTTP and SSE transports |
304
+ | `@loopstack/common` | `BaseTool`, `@Tool`, `ToolCallOptions`, `RunContext` |
305
+ | `@loopstack/core` | `LoopCoreModule` (NestJS integration) |
306
+ | `@nestjs/common`, `@nestjs/core` | Dependency injection, module system |
307
+ | `zod` | Schema validation |
208
308
 
209
- Run with:
309
+ ## Related
210
310
 
211
- ```sh
212
- npm test
213
- ```
311
+ - [`@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.
312
+ - [Agent Workflows](https://loopstack.ai/docs/build/ai/agent-workflows) — How `ChatAgentWorkflow` and tool resolution work.
313
+ - [Tool Configuration](https://loopstack.ai/docs/build/fundamentals/tools) — How `configSchema` and `options.config` are merged at call time.
314
+ - [`@loopstack/claude-module`](https://loopstack.ai/docs/registry/features/claude-module) — LLM provider that powers the agent loop calling MCP tools.
315
+
316
+ ## About
317
+
318
+ **Author:** Jakob Klippel
319
+ **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.1",
11
+ "version": "0.3.3",
12
12
  "license": "MIT",
13
13
  "author": {
14
14
  "name": "Jakob Klippel",
@@ -32,11 +32,11 @@
32
32
  "watch": "nest build --watch"
33
33
  },
34
34
  "dependencies": {
35
+ "@loopstack/common": "^0.34.0",
36
+ "@loopstack/core": "^0.34.0",
35
37
  "@modelcontextprotocol/sdk": "^1.29.0"
36
38
  },
37
39
  "devDependencies": {
38
- "@loopstack/common": "^0.32.3",
39
- "@loopstack/core": "^0.32.3",
40
40
  "@nestjs/common": "^11.1.19",
41
41
  "@nestjs/core": "^11.1.19",
42
42
  "@swc/core": "^1.15.33",
@@ -45,8 +45,6 @@
45
45
  "zod": "^4.3.6"
46
46
  },
47
47
  "peerDependencies": {
48
- "@loopstack/common": "^0.32.3",
49
- "@loopstack/core": "^0.32.3",
50
48
  "@nestjs/common": "^11.0.0",
51
49
  "@nestjs/core": "^11.0.0",
52
50
  "zod": "^4.0.0"