@graphorin/mcp 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +296 -0
- package/dist/client/adapt-result.d.ts +25 -0
- package/dist/client/adapt-result.d.ts.map +1 -0
- package/dist/client/adapt-result.js +174 -0
- package/dist/client/adapt-result.js.map +1 -0
- package/dist/client/client-handlers.js +134 -0
- package/dist/client/client-handlers.js.map +1 -0
- package/dist/client/client.d.ts +21 -0
- package/dist/client/client.d.ts.map +1 -0
- package/dist/client/client.js +358 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client/defer-loading.d.ts +7 -0
- package/dist/client/defer-loading.d.ts.map +1 -0
- package/dist/client/defer-loading.js +81 -0
- package/dist/client/defer-loading.js.map +1 -0
- package/dist/client/inbound-filters.js +65 -0
- package/dist/client/inbound-filters.js.map +1 -0
- package/dist/client/index.d.ts +7 -0
- package/dist/client/index.js +7 -0
- package/dist/client/mcp-resource-reader.d.ts +22 -0
- package/dist/client/mcp-resource-reader.d.ts.map +1 -0
- package/dist/client/mcp-resource-reader.js +113 -0
- package/dist/client/mcp-resource-reader.js.map +1 -0
- package/dist/client/pinning.js +41 -0
- package/dist/client/pinning.js.map +1 -0
- package/dist/client/to-tools.d.ts +41 -0
- package/dist/client/to-tools.d.ts.map +1 -0
- package/dist/client/to-tools.js +125 -0
- package/dist/client/to-tools.js.map +1 -0
- package/dist/client/transport-factory.js +86 -0
- package/dist/client/transport-factory.js.map +1 -0
- package/dist/client/types.d.ts +353 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/errors/index.d.ts +120 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +88 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/helpers/identity.d.ts +22 -0
- package/dist/helpers/identity.d.ts.map +1 -0
- package/dist/helpers/identity.js +85 -0
- package/dist/helpers/identity.js.map +1 -0
- package/dist/helpers/index.d.ts +3 -0
- package/dist/helpers/index.js +4 -0
- package/dist/helpers/validate-config.d.ts +16 -0
- package/dist/helpers/validate-config.d.ts.map +1 -0
- package/dist/helpers/validate-config.js +61 -0
- package/dist/helpers/validate-config.js.map +1 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +58 -0
- package/dist/index.js.map +1 -0
- package/dist/oauth/bridge.d.ts +59 -0
- package/dist/oauth/bridge.d.ts.map +1 -0
- package/dist/oauth/bridge.js +75 -0
- package/dist/oauth/bridge.js.map +1 -0
- package/dist/oauth/index.d.ts +3 -0
- package/dist/oauth/index.js +4 -0
- package/dist/oauth/library.d.ts +23 -0
- package/dist/oauth/library.d.ts.map +1 -0
- package/dist/oauth/library.js +27 -0
- package/dist/oauth/library.js.map +1 -0
- package/dist/registry/json-schema.js +215 -0
- package/dist/registry/json-schema.js.map +1 -0
- package/dist/transport/index.d.ts +4 -0
- package/dist/transport/index.js +4 -0
- package/dist/transport/types.d.ts +93 -0
- package/dist/transport/types.d.ts.map +1 -0
- package/package.json +105 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# @graphorin/mcp
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Initial release. The `@graphorin/mcp` package ships the typed
|
|
8
|
+
Model Context Protocol client used by Graphorin agents, the
|
|
9
|
+
standalone server, and the CLI. The release surface includes the
|
|
10
|
+
three transports (`stdio`, `streamable-http`, `sse`), the typed
|
|
11
|
+
`MCPClient` (`listTools` / `listResources` / `listPrompts` /
|
|
12
|
+
`callTool` / `readResource` / `getPrompt` / `close`), the
|
|
13
|
+
strategy-aware `toTools()` adapter (per-server inbound
|
|
14
|
+
prompt-injection sanitization, deferred-loading auto-default at
|
|
15
|
+
the 10-tool threshold, structured-content + outputSchema
|
|
16
|
+
round-trip with backward-compatible `TextContent` mirror,
|
|
17
|
+
per-server result envelope overrides, collision-strategy +
|
|
18
|
+
per-server priority, per-server preferredModel + side-effect
|
|
19
|
+
class overrides), the pluggable `EventStore` contract for
|
|
20
|
+
resumable Streamable HTTP sessions (`Mcp-Session-Id` +
|
|
21
|
+
`Last-Event-ID` handshake), the OAuth bridge backed by
|
|
22
|
+
`@graphorin/security/oauth`, the typed error hierarchy, and the
|
|
23
|
+
helper functions consumed by `graphorin auth` (CLI). See the
|
|
24
|
+
package `README.md` for the full surface inventory and the
|
|
25
|
+
workspace `CHANGELOG.md` for the rollup release notes.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Oleksiy Stepurenko
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# @graphorin/mcp
|
|
2
|
+
|
|
3
|
+
> Model Context Protocol client for the Graphorin framework.
|
|
4
|
+
|
|
5
|
+
`@graphorin/mcp` ships the typed MCP client used by Graphorin
|
|
6
|
+
agents, the standalone server, and the CLI. The package wraps the
|
|
7
|
+
official `@modelcontextprotocol/sdk@^1.29.0` client primitives and
|
|
8
|
+
adds the Graphorin-specific glue: the `toTools()` adapter that
|
|
9
|
+
bridges MCP tool descriptors into the typed Graphorin tool
|
|
10
|
+
registry, the inbound prompt-injection sanitization defaults for
|
|
11
|
+
MCP-derived tools, the structured-content + outputSchema
|
|
12
|
+
round-trip with backward-compatible TextContent mirror, the
|
|
13
|
+
deferred-loading auto-default at the 10-tool threshold, the
|
|
14
|
+
collision-strategy + per-server priority handoff to the tool
|
|
15
|
+
registry, honest Streamable HTTP session semantics, and the OAuth
|
|
16
|
+
bridge that resolves bearer headers from
|
|
17
|
+
the existing outbound OAuth subsystem in `@graphorin/security`.
|
|
18
|
+
|
|
19
|
+
## Highlights
|
|
20
|
+
|
|
21
|
+
- **Three transports out of the box.** `'stdio'` is the primary
|
|
22
|
+
transport for local MCP servers started as a child process;
|
|
23
|
+
`'streamable-http'` is the current default for remote servers
|
|
24
|
+
with optional Streamable HTTP session support; `'sse'` is the
|
|
25
|
+
deprecated legacy transport, kept for back-compat with servers
|
|
26
|
+
that have not migrated yet (the runtime emits one WARN per
|
|
27
|
+
process on selection).
|
|
28
|
+
- **Typed `MCPClient` surface.** `listTools` / `listResources` /
|
|
29
|
+
`listPrompts` / `callTool` / `readResource` / `getPrompt` /
|
|
30
|
+
`close` plus the strategy-aware `toTools(...)` adapter.
|
|
31
|
+
- **Strict default for MCP-derived tools.** Every `Tool` produced
|
|
32
|
+
by `MCPClient.toTools()` defaults to the
|
|
33
|
+
`'detect-and-strip-and-wrap'` inbound prompt-injection
|
|
34
|
+
sanitization policy (mirrored on tool-result bodies at execution
|
|
35
|
+
time) and the `'sandboxed'` sandbox policy. The trust class is
|
|
36
|
+
pinned to `'mcp-derived'` so the agent runtime's per-step
|
|
37
|
+
preamble fires regardless of body-level overrides.
|
|
38
|
+
- **Auto-deferral at the 10-tool threshold.** When the MCP
|
|
39
|
+
server's `listTools()` returns more than `deferLoadingThreshold`
|
|
40
|
+
entries (default `10`), the per-server default flips
|
|
41
|
+
`defer_loading: true` for every produced tool; an INFO-log
|
|
42
|
+
records the threshold and the tool names; the
|
|
43
|
+
`tool.retrieval.deferred.total` counter is incremented per
|
|
44
|
+
affected tool.
|
|
45
|
+
- **Structured-content round-trip.** When the MCP server
|
|
46
|
+
advertises an `outputSchema` on a tool definition (per the
|
|
47
|
+
current MCP spec) and returns `structuredContent` on `callTool`,
|
|
48
|
+
the adapter validates the structured form against the converted
|
|
49
|
+
Zod-compatible schema and surfaces it as the typed `Tool.execute`
|
|
50
|
+
output. The unstructured `content[]` is preserved in
|
|
51
|
+
`ToolReturn.contentParts` (including the backward-compatible
|
|
52
|
+
`TextContent` mirror); pre-spec servers that emit only `content[]`
|
|
53
|
+
fall through to the legacy concatenated-text behaviour.
|
|
54
|
+
- **Collision resolution at the registry boundary.** The client
|
|
55
|
+
exposes `serverIdentity`, `collisionStrategy` (default
|
|
56
|
+
`'auto-prefix'`), and the optional per-client `priority` field;
|
|
57
|
+
the registry consumes the trio when its strategy-aware
|
|
58
|
+
`assertNoDuplicates(strategy, ctx)` overload runs.
|
|
59
|
+
- **OAuth integration.** `createOAuthAuthorizationProvider({...})`
|
|
60
|
+
wraps the existing `refreshOAuthSession(...)` helper from
|
|
61
|
+
`@graphorin/security/oauth`, resolves the bearer header on every
|
|
62
|
+
request, debounces concurrent refreshes via the OAuth
|
|
63
|
+
subsystem's in-flight de-duplication, and emits the
|
|
64
|
+
`mcp.auth.expired` lifecycle event when a refresh fails.
|
|
65
|
+
- **Streamable HTTP sessions.** The client persists the assigned
|
|
66
|
+
`Mcp-Session-Id` and the SDK transport auto-reconnects with
|
|
67
|
+
`Last-Event-ID` after a transient disconnect. Event replay is the
|
|
68
|
+
SERVER's responsibility per the Streamable HTTP spec — the client
|
|
69
|
+
surfaces `sessionIdPresent` (stateful routing detected; not a
|
|
70
|
+
replay guarantee).
|
|
71
|
+
|
|
72
|
+
## Stable sub-paths
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
import { createMCPClient } from '@graphorin/mcp/client';
|
|
76
|
+
import { createOAuthAuthorizationProvider } from '@graphorin/mcp/oauth';
|
|
77
|
+
import {
|
|
78
|
+
formatMCPServerName,
|
|
79
|
+
validateMCPServerConfig,
|
|
80
|
+
} from '@graphorin/mcp/helpers';
|
|
81
|
+
import { MCPConnectionError, MCPProtocolError } from '@graphorin/mcp/errors';
|
|
82
|
+
import type {
|
|
83
|
+
MCPTransportConfig,
|
|
84
|
+
ServerIdentity,
|
|
85
|
+
} from '@graphorin/mcp/transport';
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Quick start
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { createMCPClient } from '@graphorin/mcp';
|
|
92
|
+
|
|
93
|
+
// Local MCP server over stdio.
|
|
94
|
+
const fileSystem = await createMCPClient({
|
|
95
|
+
transport: {
|
|
96
|
+
kind: 'stdio',
|
|
97
|
+
command: 'pnpm',
|
|
98
|
+
args: ['dlx', '@modelcontextprotocol/server-filesystem', '/Users/me/Documents'],
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Remote MCP server over Streamable HTTP.
|
|
103
|
+
const issues = await createMCPClient({
|
|
104
|
+
transport: {
|
|
105
|
+
kind: 'streamable-http',
|
|
106
|
+
url: 'https://issues.example.com/mcp',
|
|
107
|
+
headers: { Authorization: 'Bearer ${TOKEN}' },
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const tools = [
|
|
112
|
+
...(await fileSystem.toTools({ namespace: 'fs' })),
|
|
113
|
+
...(await issues.toTools({ namespace: 'issues' })),
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
// `tools` is ready to register with `@graphorin/tools`'s
|
|
117
|
+
// `createToolRegistry({...})`; the agent runtime consumes the
|
|
118
|
+
// resulting registry as part of its per-step planner.
|
|
119
|
+
|
|
120
|
+
await fileSystem.close();
|
|
121
|
+
await issues.close();
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Inbound sanitization policy reference
|
|
125
|
+
|
|
126
|
+
The per-server `inboundSanitization` field controls how
|
|
127
|
+
imperative-pattern matches in tool result bodies (and tool
|
|
128
|
+
descriptions, at registration time) are handled:
|
|
129
|
+
|
|
130
|
+
| Policy | Description |
|
|
131
|
+
|---------------------------------|-----------------------------------------------------------------------------|
|
|
132
|
+
| `'pass-through'` | No scan; bytes-equal forwarding (use only for trusted in-house servers). |
|
|
133
|
+
| `'detect-and-flag'` | Scan; emit a flag span attribute + audit row but do not modify the body. |
|
|
134
|
+
| `'detect-and-strip'` | Replace each match with the `[REDACTED:imperative-pattern]` literal token. |
|
|
135
|
+
| `'detect-and-wrap'` | Wrap the body in `<<<untrusted_content>>>` without stripping matches. |
|
|
136
|
+
| `'detect-and-strip-and-wrap'` | **Default for MCP-derived tools.** Both strip matches and wrap the body. |
|
|
137
|
+
|
|
138
|
+
Tool descriptions are always sanitized at registration time using
|
|
139
|
+
the `'detect-and-strip'` variant of the configured policy (the
|
|
140
|
+
wrap envelope is reserved for tool-result bodies inside the
|
|
141
|
+
conversation history; the description goes into the per-step tool
|
|
142
|
+
catalogue verbatim aside from the strip pass).
|
|
143
|
+
|
|
144
|
+
## OAuth integration
|
|
145
|
+
|
|
146
|
+
The package re-exports the OAuth library functions as the
|
|
147
|
+
operator-facing CLI helpers `mcpAuthLogin`, `mcpAuthListSessions`,
|
|
148
|
+
`mcpAuthRefresh`, `mcpAuthRevoke`, and `mcpAuthStatus`. They wrap
|
|
149
|
+
the lower-level `loginInteractive`, `listOAuthSessions`,
|
|
150
|
+
`refreshOAuthSession`, `revokeOAuthSession`, and `getOAuthStatus`
|
|
151
|
+
helpers in `@graphorin/security/oauth`. The corresponding CLI
|
|
152
|
+
binaries land in Phase 15.
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
import {
|
|
156
|
+
createInMemoryOAuthServerStore,
|
|
157
|
+
loginInteractive,
|
|
158
|
+
} from '@graphorin/security/oauth';
|
|
159
|
+
import {
|
|
160
|
+
createOAuthAuthorizationProvider,
|
|
161
|
+
createMCPClient,
|
|
162
|
+
} from '@graphorin/mcp';
|
|
163
|
+
|
|
164
|
+
const storage = createInMemoryOAuthServerStore();
|
|
165
|
+
await loginInteractive({
|
|
166
|
+
serverId: 'issues-mcp',
|
|
167
|
+
serverUrl: 'https://issues.example.com/mcp',
|
|
168
|
+
storage,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const authProvider = createOAuthAuthorizationProvider({
|
|
172
|
+
serverId: 'issues-mcp',
|
|
173
|
+
storage,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const issues = await createMCPClient({
|
|
177
|
+
transport: {
|
|
178
|
+
kind: 'streamable-http',
|
|
179
|
+
url: 'https://issues.example.com/mcp',
|
|
180
|
+
},
|
|
181
|
+
// The client installs a per-request fetch-wrapper that calls
|
|
182
|
+
// `authProvider.resolveHeader()` on every outgoing request, so the
|
|
183
|
+
// refresh-ahead window fires automatically and a long-lived session
|
|
184
|
+
// survives token expiry without re-creating the client. Do **not**
|
|
185
|
+
// resolve the token once into static `headers` — that pins a single
|
|
186
|
+
// token and defeats the refresh.
|
|
187
|
+
authProvider,
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
For a rare pre-shared token, pass `bearerToken` instead (mutually
|
|
192
|
+
exclusive with `authProvider`; supplying both throws
|
|
193
|
+
`MCPInvalidConfigError`):
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
const issues = await createMCPClient({
|
|
197
|
+
transport: { kind: 'streamable-http', url: 'https://issues.example.com/mcp' },
|
|
198
|
+
bearerToken: process.env.ISSUES_MCP_TOKEN!,
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Streamable HTTP sessions
|
|
203
|
+
|
|
204
|
+
When the MCP server assigns an `Mcp-Session-Id` on `initialize`, the
|
|
205
|
+
client persists it for stateful routing and exposes
|
|
206
|
+
`client.sessionIdPresent` (`client.resumable` is its deprecated
|
|
207
|
+
alias). A session id is **not** a replay guarantee: per the
|
|
208
|
+
Streamable HTTP spec, event replay is the **server's**
|
|
209
|
+
responsibility, and the SDK transport already auto-reconnects with
|
|
210
|
+
the `Last-Event-ID` header after a transient disconnect — no client
|
|
211
|
+
configuration needed. (The former client-side `eventStore` option
|
|
212
|
+
was removed: a client-held store cannot drive replay; passing the
|
|
213
|
+
legacy option logs a warning.)
|
|
214
|
+
|
|
215
|
+
### Reverse-proxy operational note
|
|
216
|
+
|
|
217
|
+
When a Graphorin client connects to a Streamable HTTP MCP server
|
|
218
|
+
through a reverse proxy (nginx, HAProxy, AWS ALB, Cloudflare, GCP
|
|
219
|
+
Load Balancer), the proxy MUST be configured to disable response
|
|
220
|
+
buffering on the SSE-style streaming response. Without the
|
|
221
|
+
directive the symptoms are:
|
|
222
|
+
|
|
223
|
+
- The client never receives `tool.execute.progress` /
|
|
224
|
+
`tool.execute.partial` events until the tool completes (defeats
|
|
225
|
+
the streaming purpose).
|
|
226
|
+
- The connection appears to hang from the client side until the
|
|
227
|
+
proxy buffer is full.
|
|
228
|
+
- Intermediate keep-alive timeouts fire because the proxy thinks
|
|
229
|
+
the connection is idle.
|
|
230
|
+
|
|
231
|
+
#### Canonical nginx snippet
|
|
232
|
+
|
|
233
|
+
```nginx
|
|
234
|
+
location /mcp {
|
|
235
|
+
proxy_buffering off;
|
|
236
|
+
proxy_cache off;
|
|
237
|
+
chunked_transfer_encoding on;
|
|
238
|
+
proxy_read_timeout 600s;
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
#### Analogous notes for other reverse proxies
|
|
243
|
+
|
|
244
|
+
- **AWS ALB.** Enable stickiness, raise the idle-timeout to at
|
|
245
|
+
least 600 s, and confirm WebSocket-style upgrade compatibility
|
|
246
|
+
is enabled.
|
|
247
|
+
- **Cloudflare.** Disable "Always Online" caching for the MCP
|
|
248
|
+
route and raise the proxy timeout via Cloudflare Tunnel.
|
|
249
|
+
- **GCP Load Balancer.** Configure the backend service with
|
|
250
|
+
`connectionDraining.drainingTimeoutSec >= 600` and
|
|
251
|
+
`timeoutSec >= 600`.
|
|
252
|
+
|
|
253
|
+
## Errors
|
|
254
|
+
|
|
255
|
+
Every error class extends `GraphorinMCPError` and carries a
|
|
256
|
+
stable lowercase `kind` discriminator and an actionable `hint`
|
|
257
|
+
field where applicable:
|
|
258
|
+
|
|
259
|
+
- `MCPConnectionError` — transport could not be established or was dropped.
|
|
260
|
+
- `MCPProtocolError` — JSON-RPC / MCP protocol-level error.
|
|
261
|
+
- `MCPAuthError` — authentication / authorization failure.
|
|
262
|
+
- `MCPToolNotFoundError` — the requested tool is not registered with the server.
|
|
263
|
+
- `MCPCallTimeoutError` — tool call exceeded the configured timeout (variant `'session-lost'` for the resume-handshake-lost path).
|
|
264
|
+
- `MCPCancelledError` — call was cancelled by an `AbortSignal`.
|
|
265
|
+
- `MCPInvalidConfigError` — the supplied `MCPTransportConfig` is invalid.
|
|
266
|
+
- `MCPTransportNotSupportedError` — the supplied configuration requested an unsupported transport / capability combination (variant `'transport-resumable-not-supported'` for resumable sessions on `'stdio'` / `'sse'`).
|
|
267
|
+
|
|
268
|
+
## Acceptance & testing
|
|
269
|
+
|
|
270
|
+
- 109 unit + integration + property tests run under Vitest with
|
|
271
|
+
no network calls (verified by the workspace
|
|
272
|
+
`pnpm run check-no-network` script).
|
|
273
|
+
- Integration tests use the SDK's `InMemoryTransport` linked-pair
|
|
274
|
+
+ a configurable in-process `Server` fixture; no child
|
|
275
|
+
processes are spawned and no network round-trips happen during
|
|
276
|
+
the test run.
|
|
277
|
+
- Coverage thresholds: 80 % statements, 80 % lines, 80 %
|
|
278
|
+
functions, 70 % branches.
|
|
279
|
+
|
|
280
|
+
## Dependencies
|
|
281
|
+
|
|
282
|
+
| Dependency | Pin | Purpose |
|
|
283
|
+
|----------------------------------|------------------|-----------------------------------------------|
|
|
284
|
+
| `@modelcontextprotocol/sdk` | `^1.29.0` | Underlying MCP client + transport primitives. |
|
|
285
|
+
| `@graphorin/core` | workspace | `Tool` / `MessageContent` / `ZodLikeSchema`. |
|
|
286
|
+
| `@graphorin/security` | workspace | OAuth client + lifecycle event surface. |
|
|
287
|
+
| `@graphorin/observability` | workspace | Imperative-pattern catalogue. |
|
|
288
|
+
| `@graphorin/tools` | workspace | Inbound sanitization helper + counter registry. |
|
|
289
|
+
|
|
290
|
+
## License
|
|
291
|
+
|
|
292
|
+
MIT — © 2026 Oleksiy Stepurenko.
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
**Project Graphorin** · v0.5.0 · MIT License · © 2026 Oleksiy Stepurenko · <https://github.com/o-stepper/graphorin>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ServerIdentity } from "../transport/types.js";
|
|
2
|
+
import { MCPCallToolResult } from "./types.js";
|
|
3
|
+
import { ToolReturn, ZodLikeSchema } from "@graphorin/core";
|
|
4
|
+
|
|
5
|
+
//#region src/client/adapt-result.d.ts
|
|
6
|
+
|
|
7
|
+
/** Arguments for {@link adaptCallResult}. */
|
|
8
|
+
interface AdaptCallResultArgs {
|
|
9
|
+
readonly result: MCPCallToolResult;
|
|
10
|
+
readonly outputSchema?: ZodLikeSchema<unknown> | undefined;
|
|
11
|
+
readonly serverIdentity: ServerIdentity;
|
|
12
|
+
readonly toolName: string;
|
|
13
|
+
readonly logger?: (level: 'debug' | 'info' | 'warn' | 'error', message: string, fields?: Record<string, unknown>) => void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Convert an MCP `CallToolResult` into a typed Graphorin `ToolReturn`,
|
|
17
|
+
* handling the structured-content + outputSchema round-trip and the
|
|
18
|
+
* backward-compatible TextContent mirror.
|
|
19
|
+
*
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
declare function adaptCallResult(args: AdaptCallResultArgs): ToolReturn<unknown>;
|
|
23
|
+
//#endregion
|
|
24
|
+
export { adaptCallResult };
|
|
25
|
+
//# sourceMappingURL=adapt-result.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapt-result.d.ts","names":[],"sources":["../../src/client/adapt-result.ts"],"sourcesContent":[],"mappings":";;;;;;;UAmBiB,mBAAA;mBACE;0BACO;2BACC;;2FAKd;;;;;;;;;iBAWG,eAAA,OAAsB,sBAAsB"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { MCPToolExecutionError } from "../errors/index.js";
|
|
2
|
+
import { incrementCounter } from "@graphorin/tools/audit";
|
|
3
|
+
|
|
4
|
+
//#region src/client/adapt-result.ts
|
|
5
|
+
/**
|
|
6
|
+
* Convert an MCP `CallToolResult` into a typed Graphorin `ToolReturn`,
|
|
7
|
+
* handling the structured-content + outputSchema round-trip and the
|
|
8
|
+
* backward-compatible TextContent mirror.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
function adaptCallResult(args) {
|
|
13
|
+
const { result, outputSchema, serverIdentity, toolName } = args;
|
|
14
|
+
if (result.isError === true) {
|
|
15
|
+
const errorText = (result.content ?? []).filter((p) => p.type === "text").map((p) => p.text).join("\n");
|
|
16
|
+
incrementCounter("mcp.call.tool-error.total", {
|
|
17
|
+
server: serverIdentity.id,
|
|
18
|
+
tool: toolName
|
|
19
|
+
});
|
|
20
|
+
throw new MCPToolExecutionError(errorText.length > 0 ? errorText : `MCP tool '${toolName}' reported an error result.`, { metadata: { tool: toolName } });
|
|
21
|
+
}
|
|
22
|
+
const contentParts = [];
|
|
23
|
+
const resourceLinkPreviews = [];
|
|
24
|
+
for (const part of result.content) {
|
|
25
|
+
if (part.type === "resource_link") {
|
|
26
|
+
incrementCounter("mcp.resource-link.emitted.total", {
|
|
27
|
+
server: serverIdentity.id,
|
|
28
|
+
tool: toolName
|
|
29
|
+
});
|
|
30
|
+
resourceLinkPreviews.push(formatResourceLinkPreview(part));
|
|
31
|
+
}
|
|
32
|
+
const messagePart = mcpContentToMessageContent(part);
|
|
33
|
+
if (messagePart !== void 0) contentParts.push(messagePart);
|
|
34
|
+
}
|
|
35
|
+
const textParts = (result.content ?? []).filter((p) => p.type === "text");
|
|
36
|
+
const outputSegments = [];
|
|
37
|
+
for (const part of result.content ?? []) switch (part.type) {
|
|
38
|
+
case "text":
|
|
39
|
+
outputSegments.push(part.text);
|
|
40
|
+
break;
|
|
41
|
+
case "image":
|
|
42
|
+
case "audio":
|
|
43
|
+
outputSegments.push(describeBinaryPart(part.type, part.mimeType, part.data));
|
|
44
|
+
break;
|
|
45
|
+
case "resource":
|
|
46
|
+
if (part.resource.text !== void 0) outputSegments.push(part.resource.text);
|
|
47
|
+
else if (part.resource.blob !== void 0) outputSegments.push(`[resource ${part.resource.uri} ${part.resource.mimeType ?? "application/octet-stream"}, ~${approxDecodedSize(part.resource.blob)} — full payload in contentParts]`);
|
|
48
|
+
else outputSegments.push(`Resource ${part.resource.uri}`);
|
|
49
|
+
break;
|
|
50
|
+
case "resource_link": break;
|
|
51
|
+
}
|
|
52
|
+
const concatenatedText = [...outputSegments, ...resourceLinkPreviews].join("\n");
|
|
53
|
+
if (result.structuredContent !== void 0) if (outputSchema !== void 0) {
|
|
54
|
+
const validation = outputSchema.safeParse(result.structuredContent);
|
|
55
|
+
if (validation.success) {
|
|
56
|
+
incrementCounter("mcp.structured-content.emitted.total", {
|
|
57
|
+
server: serverIdentity.id,
|
|
58
|
+
tool: toolName
|
|
59
|
+
});
|
|
60
|
+
const finalParts = [...contentParts];
|
|
61
|
+
if (textParts.length === 0) finalParts.push({
|
|
62
|
+
type: "text",
|
|
63
|
+
text: JSON.stringify(result.structuredContent)
|
|
64
|
+
});
|
|
65
|
+
return Object.freeze({
|
|
66
|
+
output: validation.data,
|
|
67
|
+
contentParts: Object.freeze(finalParts)
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
incrementCounter("mcp.structured-content.validation.failure.total", {
|
|
71
|
+
server: serverIdentity.id,
|
|
72
|
+
tool: toolName
|
|
73
|
+
});
|
|
74
|
+
args.logger?.("warn", "mcp.structured-content.validation.failed: payload mirrored as JSON text", {
|
|
75
|
+
server: serverIdentity.id,
|
|
76
|
+
tool: toolName
|
|
77
|
+
});
|
|
78
|
+
const fallbackParts = [...contentParts];
|
|
79
|
+
if (textParts.length === 0) fallbackParts.push({
|
|
80
|
+
type: "text",
|
|
81
|
+
text: JSON.stringify(result.structuredContent)
|
|
82
|
+
});
|
|
83
|
+
return Object.freeze({
|
|
84
|
+
output: concatenatedText.length > 0 ? concatenatedText : JSON.stringify(result.structuredContent),
|
|
85
|
+
contentParts: Object.freeze(fallbackParts)
|
|
86
|
+
});
|
|
87
|
+
} else {
|
|
88
|
+
incrementCounter("mcp.structured-content.emitted.total", {
|
|
89
|
+
server: serverIdentity.id,
|
|
90
|
+
tool: toolName
|
|
91
|
+
});
|
|
92
|
+
const finalParts = [...contentParts];
|
|
93
|
+
if (textParts.length === 0) finalParts.push({
|
|
94
|
+
type: "text",
|
|
95
|
+
text: JSON.stringify(result.structuredContent)
|
|
96
|
+
});
|
|
97
|
+
return Object.freeze({
|
|
98
|
+
output: result.structuredContent,
|
|
99
|
+
contentParts: Object.freeze(finalParts)
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return Object.freeze({
|
|
103
|
+
output: concatenatedText,
|
|
104
|
+
contentParts: Object.freeze(contentParts)
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
function mcpContentToMessageContent(part) {
|
|
108
|
+
switch (part.type) {
|
|
109
|
+
case "text": return {
|
|
110
|
+
type: "text",
|
|
111
|
+
text: part.text
|
|
112
|
+
};
|
|
113
|
+
case "image": return {
|
|
114
|
+
type: "image",
|
|
115
|
+
image: decodeBase64(part.data),
|
|
116
|
+
mimeType: part.mimeType
|
|
117
|
+
};
|
|
118
|
+
case "audio": return {
|
|
119
|
+
type: "audio",
|
|
120
|
+
audio: decodeBase64(part.data),
|
|
121
|
+
mimeType: part.mimeType
|
|
122
|
+
};
|
|
123
|
+
case "resource": {
|
|
124
|
+
const text = part.resource.text;
|
|
125
|
+
if (text !== void 0) return {
|
|
126
|
+
type: "text",
|
|
127
|
+
text
|
|
128
|
+
};
|
|
129
|
+
const blob = part.resource.blob;
|
|
130
|
+
if (blob !== void 0) return {
|
|
131
|
+
type: "file",
|
|
132
|
+
file: decodeBase64(blob),
|
|
133
|
+
mimeType: part.resource.mimeType ?? "application/octet-stream"
|
|
134
|
+
};
|
|
135
|
+
return {
|
|
136
|
+
type: "text",
|
|
137
|
+
text: `Resource ${part.resource.uri}`
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
case "resource_link": return {
|
|
141
|
+
type: "text",
|
|
142
|
+
text: formatResourceLinkPreview(part)
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Render the compact, model-facing preview for a `resource_link`. The
|
|
148
|
+
* `uri` doubles as the `read_result` handle — an
|
|
149
|
+
* {@link import('./mcp-resource-reader.js').createMcpResourceReader}
|
|
150
|
+
* resolves it on demand via {@link MCPClient.readResource}.
|
|
151
|
+
*/
|
|
152
|
+
function formatResourceLinkPreview(part) {
|
|
153
|
+
const label = part.title === void 0 || part.title.length === 0 ? part.name : part.title;
|
|
154
|
+
const meta = [];
|
|
155
|
+
if (part.mimeType !== void 0) meta.push(part.mimeType);
|
|
156
|
+
if (part.size !== void 0) meta.push(`${part.size} bytes`);
|
|
157
|
+
return `[resource_link] ${label}${meta.length === 0 ? "" : ` (${meta.join(", ")})`}${part.description === void 0 || part.description.length === 0 ? "" : ` — ${part.description}`}\nFetch the full contents on demand with read_result, handle: ${part.uri}`;
|
|
158
|
+
}
|
|
159
|
+
/** Human-readable size of a base64 payload's decoded bytes (MC-8). */
|
|
160
|
+
function approxDecodedSize(base64) {
|
|
161
|
+
const bytes = Math.floor(base64.length * 3 / 4);
|
|
162
|
+
return bytes >= 1024 ? `${Math.round(bytes / 1024)}kB` : `${bytes}B`;
|
|
163
|
+
}
|
|
164
|
+
/** Text descriptor for a non-text content part (MC-8). */
|
|
165
|
+
function describeBinaryPart(kind, mimeType, data) {
|
|
166
|
+
return `[${kind} ${mimeType}, ~${approxDecodedSize(data)} — full payload in contentParts]`;
|
|
167
|
+
}
|
|
168
|
+
function decodeBase64(value) {
|
|
169
|
+
return Uint8Array.from(Buffer.from(value, "base64"));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
//#endregion
|
|
173
|
+
export { adaptCallResult };
|
|
174
|
+
//# sourceMappingURL=adapt-result.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapt-result.js","names":["contentParts: MessageContent[]","resourceLinkPreviews: string[]","outputSegments: string[]","meta: string[]"],"sources":["../../src/client/adapt-result.ts"],"sourcesContent":["/**\n * Result adaptation for the `toTools()` adapter (extracted from\n * `to-tools.ts` per F-MCP-001).\n *\n * Converts an MCP `CallToolResult` into a typed Graphorin `ToolReturn`,\n * handling the structured-content + outputSchema round-trip and the\n * backward-compatible TextContent mirror, and mapping each MCP content\n * part onto a Graphorin {@link MessageContent}.\n *\n * @packageDocumentation\n */\n\nimport type { MessageContent, ToolReturn, ZodLikeSchema } from '@graphorin/core';\nimport { incrementCounter } from '@graphorin/tools/audit';\nimport { MCPToolExecutionError } from '../errors/index.js';\nimport type { ServerIdentity } from '../transport/types.js';\nimport type { MCPCallToolResult, MCPContentPart } from './types.js';\n\n/** Arguments for {@link adaptCallResult}. */\nexport interface AdaptCallResultArgs {\n readonly result: MCPCallToolResult;\n readonly outputSchema?: ZodLikeSchema<unknown> | undefined;\n readonly serverIdentity: ServerIdentity;\n readonly toolName: string;\n readonly logger?: (\n level: 'debug' | 'info' | 'warn' | 'error',\n message: string,\n fields?: Record<string, unknown>,\n ) => void;\n}\n\n/**\n * Convert an MCP `CallToolResult` into a typed Graphorin `ToolReturn`,\n * handling the structured-content + outputSchema round-trip and the\n * backward-compatible TextContent mirror.\n *\n * @internal\n */\nexport function adaptCallResult(args: AdaptCallResultArgs): ToolReturn<unknown> {\n const { result, outputSchema, serverIdentity, toolName } = args;\n // MC-4: the SDK deliberately does NOT throw on isError results — the\n // failure marker must not launder into a successful ToolReturn. Throw\n // the typed error so the executor records a real failure; the server's\n // text rides in the message for model self-correction.\n if (result.isError === true) {\n const errorText = (result.content ?? [])\n .filter((p): p is { type: 'text'; text: string } => p.type === 'text')\n .map((p) => p.text)\n .join('\\n');\n incrementCounter('mcp.call.tool-error.total', {\n server: serverIdentity.id,\n tool: toolName,\n });\n throw new MCPToolExecutionError(\n errorText.length > 0 ? errorText : `MCP tool '${toolName}' reported an error result.`,\n { metadata: { tool: toolName } },\n );\n }\n const contentParts: MessageContent[] = [];\n // `resource_link` parts are NOT inlined: each contributes a compact\n // preview (carrying the `uri` as a result handle) so the model fetches\n // the body on demand via `read_result` instead of inflating context.\n const resourceLinkPreviews: string[] = [];\n for (const part of result.content) {\n if (part.type === 'resource_link') {\n incrementCounter('mcp.resource-link.emitted.total', {\n server: serverIdentity.id,\n tool: toolName,\n });\n resourceLinkPreviews.push(formatResourceLinkPreview(part));\n }\n const messagePart = mcpContentToMessageContent(part);\n if (messagePart !== undefined) contentParts.push(messagePart);\n }\n const textParts = (result.content ?? []).filter(\n (p): p is { type: 'text'; text: string } => p.type === 'text',\n );\n // MC-8: the typed `output` is the ONLY channel the agent loop\n // serialises into the tool message — non-text parts must leave a\n // text trace there (the full payloads stay in `contentParts`), and\n // embedded resource TEXT joins the concatenation outright.\n const outputSegments: string[] = [];\n for (const part of result.content ?? []) {\n switch (part.type) {\n case 'text':\n outputSegments.push(part.text);\n break;\n case 'image':\n case 'audio':\n outputSegments.push(describeBinaryPart(part.type, part.mimeType, part.data));\n break;\n case 'resource': {\n if (part.resource.text !== undefined) {\n outputSegments.push(part.resource.text);\n } else if (part.resource.blob !== undefined) {\n outputSegments.push(\n `[resource ${part.resource.uri} ${part.resource.mimeType ?? 'application/octet-stream'}, ~${approxDecodedSize(part.resource.blob)} — full payload in contentParts]`,\n );\n } else {\n outputSegments.push(`Resource ${part.resource.uri}`);\n }\n break;\n }\n case 'resource_link':\n // Joined below via the read_result preview.\n break;\n }\n }\n const concatenatedText = [...outputSegments, ...resourceLinkPreviews].join('\\n');\n\n if (result.structuredContent !== undefined) {\n if (outputSchema !== undefined) {\n const validation = outputSchema.safeParse(result.structuredContent);\n if (validation.success) {\n incrementCounter('mcp.structured-content.emitted.total', {\n server: serverIdentity.id,\n tool: toolName,\n });\n const finalParts = [...contentParts];\n if (textParts.length === 0) {\n finalParts.push({ type: 'text', text: JSON.stringify(result.structuredContent) });\n }\n return Object.freeze({\n output: validation.data,\n contentParts: Object.freeze(finalParts),\n });\n }\n incrementCounter('mcp.structured-content.validation.failure.total', {\n server: serverIdentity.id,\n tool: toolName,\n });\n // MC-11: a schema mismatch must not silently drop the payload —\n // log and mirror the structured content as JSON text, exactly\n // like the no-schema branch does.\n args.logger?.(\n 'warn',\n 'mcp.structured-content.validation.failed: payload mirrored as JSON text',\n { server: serverIdentity.id, tool: toolName },\n );\n const fallbackParts = [...contentParts];\n if (textParts.length === 0) {\n fallbackParts.push({ type: 'text', text: JSON.stringify(result.structuredContent) });\n }\n return Object.freeze({\n output:\n concatenatedText.length > 0 ? concatenatedText : JSON.stringify(result.structuredContent),\n contentParts: Object.freeze(fallbackParts),\n });\n } else {\n incrementCounter('mcp.structured-content.emitted.total', {\n server: serverIdentity.id,\n tool: toolName,\n });\n const finalParts = [...contentParts];\n if (textParts.length === 0) {\n finalParts.push({ type: 'text', text: JSON.stringify(result.structuredContent) });\n }\n return Object.freeze({\n output: result.structuredContent,\n contentParts: Object.freeze(finalParts),\n });\n }\n }\n\n return Object.freeze({\n output: concatenatedText,\n contentParts: Object.freeze(contentParts),\n });\n}\n\nfunction mcpContentToMessageContent(part: MCPContentPart): MessageContent | undefined {\n switch (part.type) {\n case 'text':\n return { type: 'text', text: part.text };\n case 'image':\n return {\n type: 'image',\n image: decodeBase64(part.data),\n mimeType: part.mimeType,\n };\n case 'audio':\n return {\n type: 'audio',\n audio: decodeBase64(part.data),\n mimeType: part.mimeType,\n };\n case 'resource': {\n const text = part.resource.text;\n if (text !== undefined) {\n return { type: 'text', text };\n }\n const blob = part.resource.blob;\n if (blob !== undefined) {\n return {\n type: 'file',\n file: decodeBase64(blob),\n mimeType: part.resource.mimeType ?? 'application/octet-stream',\n };\n }\n return { type: 'text', text: `Resource ${part.resource.uri}` };\n }\n case 'resource_link':\n return { type: 'text', text: formatResourceLinkPreview(part) };\n }\n}\n\n/**\n * Render the compact, model-facing preview for a `resource_link`. The\n * `uri` doubles as the `read_result` handle — an\n * {@link import('./mcp-resource-reader.js').createMcpResourceReader}\n * resolves it on demand via {@link MCPClient.readResource}.\n */\nfunction formatResourceLinkPreview(\n part: Extract<MCPContentPart, { type: 'resource_link' }>,\n): string {\n const label = part.title === undefined || part.title.length === 0 ? part.name : part.title;\n const meta: string[] = [];\n if (part.mimeType !== undefined) meta.push(part.mimeType);\n if (part.size !== undefined) meta.push(`${part.size} bytes`);\n const metaStr = meta.length === 0 ? '' : ` (${meta.join(', ')})`;\n const desc =\n part.description === undefined || part.description.length === 0 ? '' : ` — ${part.description}`;\n return `[resource_link] ${label}${metaStr}${desc}\\nFetch the full contents on demand with read_result, handle: ${part.uri}`;\n}\n\n/** Human-readable size of a base64 payload's decoded bytes (MC-8). */\nfunction approxDecodedSize(base64: string): string {\n const bytes = Math.floor((base64.length * 3) / 4);\n return bytes >= 1024 ? `${Math.round(bytes / 1024)}kB` : `${bytes}B`;\n}\n\n/** Text descriptor for a non-text content part (MC-8). */\nfunction describeBinaryPart(kind: 'image' | 'audio', mimeType: string, data: string): string {\n return `[${kind} ${mimeType}, ~${approxDecodedSize(data)} — full payload in contentParts]`;\n}\n\nfunction decodeBase64(value: string): Uint8Array {\n return Uint8Array.from(Buffer.from(value, 'base64'));\n}\n"],"mappings":"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"}
|