@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 +242 -136
- package/dist/tools/mcp-call.tool.d.ts +2 -2
- package/dist/tools/mcp-call.tool.d.ts.map +1 -1
- package/dist/tools/mcp-call.tool.js.map +1 -1
- package/dist/tools/mcp-list-tools.tool.d.ts +2 -2
- package/dist/tools/mcp-list-tools.tool.d.ts.map +1 -1
- package/dist/tools/mcp-list-tools.tool.js.map +1 -1
- package/package.json +3 -5
package/README.md
CHANGED
|
@@ -1,160 +1,164 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12
|
+
## When to Use
|
|
28
13
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
38
|
-
flow through headers.
|
|
19
|
+
## Installation
|
|
39
20
|
|
|
40
|
-
|
|
21
|
+
```sh
|
|
22
|
+
npm install @loopstack/mcp-module
|
|
23
|
+
```
|
|
41
24
|
|
|
42
|
-
|
|
25
|
+
Register the module with `McpModule.forRoot()`:
|
|
43
26
|
|
|
44
27
|
```ts
|
|
45
|
-
|
|
28
|
+
import { Module } from '@nestjs/common';
|
|
29
|
+
import { McpModule } from '@loopstack/mcp-module';
|
|
46
30
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
76
|
+
// mcp-linear.workflow.ts
|
|
77
|
+
import { z } from 'zod';
|
|
99
78
|
import { ChatAgentWorkflow } from '@loopstack/agent';
|
|
100
|
-
import {
|
|
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
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
@
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
141
|
-
allowlisted hosts within the same chat.
|
|
137
|
+
### Security Model
|
|
142
138
|
|
|
143
|
-
|
|
139
|
+
Every connection passes three checks:
|
|
144
140
|
|
|
145
|
-
`serverUrl`
|
|
146
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
198
|
-
other than `process.env` (e.g. a secrets manager).
|
|
286
|
+
## Public API
|
|
199
287
|
|
|
200
|
-
|
|
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
|
-
|
|
299
|
+
## Dependencies
|
|
203
300
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
309
|
+
## Related
|
|
210
310
|
|
|
211
|
-
|
|
212
|
-
|
|
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 {
|
|
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:
|
|
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,
|
|
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,
|
|
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 {
|
|
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:
|
|
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,
|
|
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,
|
|
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.
|
|
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"
|