@smthrs/mcp 0.0.0-stage → 1.0.0-rc.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/CHANGELOG.md +121 -0
- package/LICENSE +21 -0
- package/README.md +111 -2
- package/dist/cjs/Diagnostics.d.ts +46 -0
- package/dist/cjs/Diagnostics.d.ts.map +1 -0
- package/dist/cjs/Diagnostics.js +29 -0
- package/dist/cjs/Diagnostics.js.map +7 -0
- package/dist/cjs/McpClient.d.ts +304 -0
- package/dist/cjs/McpClient.d.ts.map +1 -0
- package/dist/cjs/McpClient.js +622 -0
- package/dist/cjs/McpClient.js.map +7 -0
- package/dist/cjs/McpError.d.ts +44 -0
- package/dist/cjs/McpError.d.ts.map +1 -0
- package/dist/cjs/McpError.js +41 -0
- package/dist/cjs/McpError.js.map +7 -0
- package/dist/cjs/McpFlows.d.ts +112 -0
- package/dist/cjs/McpFlows.d.ts.map +1 -0
- package/dist/cjs/McpFlows.js +127 -0
- package/dist/cjs/McpFlows.js.map +7 -0
- package/dist/cjs/index.d.ts +39 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +41 -0
- package/dist/cjs/index.js.map +7 -0
- package/dist/cjs/internal/DiagnosticReporter.d.ts +17 -0
- package/dist/cjs/internal/DiagnosticReporter.d.ts.map +1 -0
- package/dist/cjs/internal/DiagnosticReporter.js +56 -0
- package/dist/cjs/internal/DiagnosticReporter.js.map +7 -0
- package/dist/cjs/internal/HttpTransport.d.ts +67 -0
- package/dist/cjs/internal/HttpTransport.d.ts.map +1 -0
- package/dist/cjs/internal/HttpTransport.js +298 -0
- package/dist/cjs/internal/HttpTransport.js.map +7 -0
- package/dist/cjs/internal/JsonLimits.d.ts +29 -0
- package/dist/cjs/internal/JsonLimits.d.ts.map +1 -0
- package/dist/cjs/internal/JsonLimits.js +51 -0
- package/dist/cjs/internal/JsonLimits.js.map +7 -0
- package/dist/cjs/internal/Limits.d.ts +36 -0
- package/dist/cjs/internal/Limits.d.ts.map +1 -0
- package/dist/cjs/internal/Limits.js +34 -0
- package/dist/cjs/internal/Limits.js.map +7 -0
- package/dist/cjs/internal/Rpc.d.ts +141 -0
- package/dist/cjs/internal/Rpc.d.ts.map +1 -0
- package/dist/cjs/internal/Rpc.js +92 -0
- package/dist/cjs/internal/Rpc.js.map +7 -0
- package/dist/cjs/internal/StdioTransport.d.ts +78 -0
- package/dist/cjs/internal/StdioTransport.d.ts.map +1 -0
- package/dist/cjs/internal/StdioTransport.js +310 -0
- package/dist/cjs/internal/StdioTransport.js.map +7 -0
- package/dist/cjs/internal/Transport.d.ts +87 -0
- package/dist/cjs/internal/Transport.d.ts.map +1 -0
- package/dist/cjs/internal/Transport.js +116 -0
- package/dist/cjs/internal/Transport.js.map +7 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/Diagnostics.d.ts +46 -0
- package/dist/esm/Diagnostics.d.ts.map +1 -0
- package/dist/esm/Diagnostics.js +26 -0
- package/dist/esm/Diagnostics.js.map +1 -0
- package/dist/esm/McpClient.d.ts +304 -0
- package/dist/esm/McpClient.d.ts.map +1 -0
- package/dist/esm/McpClient.js +671 -0
- package/dist/esm/McpClient.js.map +1 -0
- package/dist/esm/McpError.d.ts +44 -0
- package/dist/esm/McpError.d.ts.map +1 -0
- package/dist/esm/McpError.js +43 -0
- package/dist/esm/McpError.js.map +1 -0
- package/dist/esm/McpFlows.d.ts +112 -0
- package/dist/esm/McpFlows.d.ts.map +1 -0
- package/dist/esm/McpFlows.js +167 -0
- package/dist/esm/McpFlows.js.map +1 -0
- package/dist/esm/index.d.ts +39 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +39 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/DiagnosticReporter.d.ts +17 -0
- package/dist/esm/internal/DiagnosticReporter.d.ts.map +1 -0
- package/dist/esm/internal/DiagnosticReporter.js +44 -0
- package/dist/esm/internal/DiagnosticReporter.js.map +1 -0
- package/dist/esm/internal/HttpTransport.d.ts +67 -0
- package/dist/esm/internal/HttpTransport.d.ts.map +1 -0
- package/dist/esm/internal/HttpTransport.js +266 -0
- package/dist/esm/internal/HttpTransport.js.map +1 -0
- package/dist/esm/internal/JsonLimits.d.ts +29 -0
- package/dist/esm/internal/JsonLimits.d.ts.map +1 -0
- package/dist/esm/internal/JsonLimits.js +55 -0
- package/dist/esm/internal/JsonLimits.js.map +1 -0
- package/dist/esm/internal/Limits.d.ts +36 -0
- package/dist/esm/internal/Limits.d.ts.map +1 -0
- package/dist/esm/internal/Limits.js +41 -0
- package/dist/esm/internal/Limits.js.map +1 -0
- package/dist/esm/internal/Rpc.d.ts +141 -0
- package/dist/esm/internal/Rpc.d.ts.map +1 -0
- package/dist/esm/internal/Rpc.js +129 -0
- package/dist/esm/internal/Rpc.js.map +1 -0
- package/dist/esm/internal/StdioTransport.d.ts +78 -0
- package/dist/esm/internal/StdioTransport.d.ts.map +1 -0
- package/dist/esm/internal/StdioTransport.js +332 -0
- package/dist/esm/internal/StdioTransport.js.map +1 -0
- package/dist/esm/internal/Transport.d.ts +87 -0
- package/dist/esm/internal/Transport.d.ts.map +1 -0
- package/dist/esm/internal/Transport.js +146 -0
- package/dist/esm/internal/Transport.js.map +1 -0
- package/docs/README.md +139 -0
- package/docs/api.md +469 -0
- package/docs/concepts/the-session.md +135 -0
- package/docs/concepts/tools-as-flows.md +116 -0
- package/docs/guides/bound-an-untrusted-server.md +158 -0
- package/docs/guides/configure-servers-for-the-cli.md +167 -0
- package/docs/guides/connect-a-server.md +161 -0
- package/docs/guides/grant-authority-to-mcp-tools.md +130 -0
- package/docs/guides/handle-a-failed-tool-call.md +125 -0
- package/docs/guides/select-the-tools-a-run-sees.md +92 -0
- package/docs/guides/testing.md +132 -0
- package/docs/guides/validate-structured-output.md +103 -0
- package/docs/installation.md +117 -0
- package/docs/quickstart.md +200 -0
- package/docs/troubleshooting.md +316 -0
- package/package.json +157 -3
- package/src/Diagnostics.ts +47 -0
- package/src/McpClient.ts +985 -0
- package/src/McpError.ts +52 -0
- package/src/McpFlows.ts +210 -0
- package/src/index.ts +42 -0
- package/src/internal/DiagnosticReporter.ts +47 -0
- package/src/internal/HttpTransport.ts +400 -0
- package/src/internal/JsonLimits.ts +53 -0
- package/src/internal/Limits.ts +48 -0
- package/src/internal/Rpc.ts +219 -0
- package/src/internal/StdioTransport.ts +491 -0
- package/src/internal/Transport.ts +178 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Validate structured output"
|
|
3
|
+
description: "What the client checks when a tool declares an outputSchema and returns structuredContent: the five supported JSON Schema keywords, what is ignored, and how a violation is reported."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 5
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
A tool may declare an `outputSchema` and answer with `structuredContent`. When
|
|
9
|
+
both are present, this client validates the value against that schema before
|
|
10
|
+
returning it, so a downstream consumer never sees structured output the server's
|
|
11
|
+
own contract rejects.
|
|
12
|
+
|
|
13
|
+
You do not switch this on. It applies whenever the catalog entry declared a
|
|
14
|
+
schema.
|
|
15
|
+
|
|
16
|
+
## What is checked
|
|
17
|
+
|
|
18
|
+
Output schemas are compiled once at catalog load. Type unions use Effect
|
|
19
|
+
Schema, with compatibility checks for exactly five supported keywords:
|
|
20
|
+
|
|
21
|
+
| Keyword | Behavior |
|
|
22
|
+
| ------------ | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
23
|
+
| `type` | All seven JSON Schema type names (`null`, `boolean`, `object`, `array`, `number`, `string`, `integer`), and arrays of names. |
|
|
24
|
+
| `required` | Each named string property must be an own property of the object. |
|
|
25
|
+
| `properties` | Each declared property present in the value is validated against its subschema. |
|
|
26
|
+
| `items` | A single schema applied to every element of an array. Tuple form is not supported. |
|
|
27
|
+
| `enum` | The value must equal one of the declared values, compared by JSON value rather than by reference. |
|
|
28
|
+
|
|
29
|
+
Validation recurses through `properties` and `items`, so a schema three levels
|
|
30
|
+
deep is checked three levels deep. Types, required names, child validators and
|
|
31
|
+
enum membership indexes are retained from the frozen catalog, including
|
|
32
|
+
composite JSON enum values. Compilation and result
|
|
33
|
+
validation yield between bounded work slices so host timers and Effect
|
|
34
|
+
interruption can run during large checks. The request timeout still covers the
|
|
35
|
+
transport request. An outer Effect timeout can also interrupt result validation.
|
|
36
|
+
|
|
37
|
+
## What is ignored
|
|
38
|
+
|
|
39
|
+
Every other keyword. `minLength`, `pattern`, `additionalProperties`, `oneOf`,
|
|
40
|
+
`$ref`, and the rest are skipped. `integer` accepts every number for which
|
|
41
|
+
`Number.isInteger` is true, including values outside the safe-integer range.
|
|
42
|
+
This compatibility contract is narrower than a general JSON Schema importer.
|
|
43
|
+
|
|
44
|
+
This is a deliberate choice, not an oversight: a partial validator that rejected
|
|
45
|
+
data for a constraint it does not implement would be worse than one that admits
|
|
46
|
+
what it cannot judge. A tool whose contract depends on `pattern` should validate
|
|
47
|
+
its own output.
|
|
48
|
+
|
|
49
|
+
## How a violation is reported
|
|
50
|
+
|
|
51
|
+
A violation fails with `invalid_response`, naming the expected constraint but
|
|
52
|
+
withholding the property path:
|
|
53
|
+
|
|
54
|
+
```text
|
|
55
|
+
MCP server "reports" returned structuredContent that its own outputSchema
|
|
56
|
+
rejects: expected number; property path withheld
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Neither property names nor values are safe to echo into model-facing errors.
|
|
60
|
+
A trusted host may inspect the path and rejected response through the optional
|
|
61
|
+
[private diagnostic observer](../api.md#diagnostics). Its details are bounded
|
|
62
|
+
and wrapped in `Redacted`, not implicitly logged.
|
|
63
|
+
|
|
64
|
+
Validation never mutates or replaces the server's value. It either passes it
|
|
65
|
+
through unchanged or fails.
|
|
66
|
+
|
|
67
|
+
## The shapes a result may take
|
|
68
|
+
|
|
69
|
+
Three combinations are all valid:
|
|
70
|
+
|
|
71
|
+
| Result | Outcome |
|
|
72
|
+
| ------------------------ | ------------------------------------------------------ |
|
|
73
|
+
| `content` only | Returned as sent, with `structuredContent: undefined`. |
|
|
74
|
+
| `structuredContent` only | Accepted, and returned with `content: []`. |
|
|
75
|
+
| Both | Both returned. |
|
|
76
|
+
|
|
77
|
+
A result carrying neither fails with `invalid_response`. A `content` value that
|
|
78
|
+
is not an array, a `content` element that is not an object, an `isError` that is
|
|
79
|
+
not a boolean, or a `structuredContent` that is not a JSON object all fail the
|
|
80
|
+
same way.
|
|
81
|
+
|
|
82
|
+
Without a declared `outputSchema`, any JSON-object `structuredContent` passes
|
|
83
|
+
through unchanged.
|
|
84
|
+
|
|
85
|
+
## Reading it downstream
|
|
86
|
+
|
|
87
|
+
`McpFlows.Result` carries the field through the flow's output schema, so a cell
|
|
88
|
+
sees it directly:
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
const report = await ctx.call("mcp/reports/summary", { period: "2026-Q1" })
|
|
92
|
+
const rows = report.structuredContent?.rows ?? []
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`structuredContent` is optional on the flow output, so a tool that never sends
|
|
96
|
+
one produces a result with the field absent rather than null.
|
|
97
|
+
|
|
98
|
+
## Next
|
|
99
|
+
|
|
100
|
+
- [Handle a failed tool call](./handle-a-failed-tool-call.md): the other
|
|
101
|
+
failures a call can produce.
|
|
102
|
+
- [Bound an untrusted server](./bound-an-untrusted-server.md): the limits that
|
|
103
|
+
apply before validation ever runs.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Installation"
|
|
3
|
+
description: "Install @smthrs/mcp, the platform services a stdio connection requires, the import forms, and the packages a host composition adds."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 1
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Availability
|
|
9
|
+
|
|
10
|
+
`@smthrs/mcp` is not published to npm yet. Its source is on
|
|
11
|
+
[GitHub](https://github.com/smithersai/smithers), and the `smthrs` command line
|
|
12
|
+
is the worked example of everything on this page: it composes this package
|
|
13
|
+
behind `--mcp-config`.
|
|
14
|
+
|
|
15
|
+
Not on npm yet; see [Installation](https://github.com/smithersai/smithers/blob/main/packages/smithers/flows/flow/docs/installation.md#use-the-libraries).
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
- Node.js 26.4.0 or later.
|
|
20
|
+
- [`effect`](https://effect.website) 4.0.0-rc.115, a peer dependency and the
|
|
21
|
+
version this package is built against. Connection and tool-call operations
|
|
22
|
+
return an `Effect`, and every schema is an `effect/Schema`. `McpFlows.mcp`
|
|
23
|
+
is synchronous and returns a `FlowBinding.Source`; `Diagnostics.layer` is a
|
|
24
|
+
`Layer` constructor.
|
|
25
|
+
|
|
26
|
+
The package ships as both ESM and CommonJS with TypeScript declarations. The
|
|
27
|
+
`@smthrs/*` packages the adapter composes are ordinary dependencies and install
|
|
28
|
+
with it.
|
|
29
|
+
|
|
30
|
+
## Provide a process spawner
|
|
31
|
+
|
|
32
|
+
Every constructor that opens a connection requires two services from the
|
|
33
|
+
caller's environment:
|
|
34
|
+
|
|
35
|
+
| Requirement | Why |
|
|
36
|
+
| --------------------- | --------------------------------------------------------------------------------------- |
|
|
37
|
+
| `ChildProcessSpawner` | An MCP server is a subprocess. The spawner is Effect's process port, not a Node import. |
|
|
38
|
+
| `Scope` | The connection is scoped. Closing the scope tears the server process down. |
|
|
39
|
+
|
|
40
|
+
On Node, `@effect/platform-node` provides the spawner:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pnpm add @effect/platform-node@4.0.0-rc.115 @effect/platform-node-shared@4.0.0-rc.115
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import { NodeServices } from "@effect/platform-node"
|
|
48
|
+
import { Effect } from "effect"
|
|
49
|
+
|
|
50
|
+
const runnable = Effect.provide(Effect.scoped(program), NodeServices.layer)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
These are the same services a host already provides for
|
|
54
|
+
`StandardFlows.shell`, so a composition that runs shell commands already has
|
|
55
|
+
them.
|
|
56
|
+
|
|
57
|
+
## Import forms
|
|
58
|
+
|
|
59
|
+
The root entry point re-exports every module as a namespace:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { McpClient, McpError, McpFlows } from "@smthrs/mcp"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Each module is also importable from its own subpath, which is the form the
|
|
66
|
+
[API reference](./api.md) uses in its examples:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import * as McpClient from "@smthrs/mcp/McpClient"
|
|
70
|
+
import * as McpFlows from "@smthrs/mcp/McpFlows"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`McpError` is a namespace either way. The error class is `McpError.McpError`:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import * as McpError from "@smthrs/mcp/McpError"
|
|
77
|
+
|
|
78
|
+
const failure = new McpError.McpError({ code: "timeout", message: "...", server: "github" })
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Two subpath forms are not public: `@smthrs/mcp/internal/*` and
|
|
82
|
+
`@smthrs/mcp/*/index`. Both are blocked in the package's export map, so the
|
|
83
|
+
JSON-RPC codec and the stdio transport are not importable.
|
|
84
|
+
`@smthrs/mcp/package.json` is exported.
|
|
85
|
+
|
|
86
|
+
## What a host composition adds
|
|
87
|
+
|
|
88
|
+
The projected `FlowBinding.Source` is only useful to something that composes a
|
|
89
|
+
flow catalog. [`@smthrs/harness`](/api/harness),
|
|
90
|
+
[`@smthrs/capability`](/api/capability), and [`@smthrs/core`](/api/core)
|
|
91
|
+
are runtime dependencies of this package and install with it:
|
|
92
|
+
|
|
93
|
+
- `@smthrs/harness` owns `FlowBinding`, the contract this package implements.
|
|
94
|
+
`FlowBinding.catalog` merges this source with the others a host offers. See
|
|
95
|
+
[Bind flows](/pkg/harness/guides/bind-flows).
|
|
96
|
+
- `@smthrs/capability` owns the action vocabulary `McpFlows.capabilities` is
|
|
97
|
+
derived from.
|
|
98
|
+
|
|
99
|
+
What a host adds is the thing that runs the cell loop:
|
|
100
|
+
|
|
101
|
+
The runtime `@smthrs/*` dependencies of this package are `@smthrs/canonical`,
|
|
102
|
+
`@smthrs/capability`, `@smthrs/core`, `@smthrs/harness`, `@smthrs/journal`,
|
|
103
|
+
and `@smthrs/kernel`. `@smthrs/registry` and `@smthrs/model` are development
|
|
104
|
+
dependencies only. A host that imports any of them directly declares it in its
|
|
105
|
+
own manifest.
|
|
106
|
+
|
|
107
|
+
[`@smthrs/agent`](/api/agent) takes the projected sources as its `flows` option,
|
|
108
|
+
alongside `StandardFlows.filesystem` and the rest.
|
|
109
|
+
|
|
110
|
+
The CLI already wires all of this. `smthrs --mcp-config <path>` connects every
|
|
111
|
+
server the file names and adds its flows to a run's catalog; see
|
|
112
|
+
[Configure servers for the CLI](./guides/configure-servers-for-the-cli.md).
|
|
113
|
+
|
|
114
|
+
## Next step
|
|
115
|
+
|
|
116
|
+
Connect to a real server and call one of its tools in the
|
|
117
|
+
[Quickstart](./quickstart.md).
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Quickstart"
|
|
3
|
+
description: "Connect to a real MCP server over stdio, call one of its tools, and read the flows the adapter projects from its catalog."
|
|
4
|
+
sidebar:
|
|
5
|
+
order: 2
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
This quickstart connects to a real MCP server in its own process, calls two of
|
|
9
|
+
its tools, and prints the flows the adapter projects from its catalog. Nothing
|
|
10
|
+
is mocked: the server is a separate program speaking newline-delimited JSON-RPC,
|
|
11
|
+
which is the only thing MCP is.
|
|
12
|
+
|
|
13
|
+
By the end you will have seen both halves of this package. `McpClient` owns the
|
|
14
|
+
session, and `McpFlows` turns that session's catalog into flows a Smithers run
|
|
15
|
+
can call.
|
|
16
|
+
|
|
17
|
+
## Prerequisites
|
|
18
|
+
|
|
19
|
+
Node.js 26.4.0 or later, and a project that depends on `@smthrs/mcp`,
|
|
20
|
+
`@effect/platform-node`, and `effect`. See
|
|
21
|
+
[Installation](./installation.md).
|
|
22
|
+
|
|
23
|
+
## Write the server
|
|
24
|
+
|
|
25
|
+
Create `quickstart-server.mjs`. It imports nothing, which is the point: the
|
|
26
|
+
process on the other end of an MCP session need not be yours.
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
import { createInterface } from "node:readline"
|
|
30
|
+
|
|
31
|
+
const tools = [
|
|
32
|
+
{
|
|
33
|
+
name: "word_count",
|
|
34
|
+
description: "Counts the words in a piece of text",
|
|
35
|
+
inputSchema: {
|
|
36
|
+
type: "object",
|
|
37
|
+
properties: { text: { type: "string" } },
|
|
38
|
+
required: ["text"]
|
|
39
|
+
},
|
|
40
|
+
outputSchema: {
|
|
41
|
+
type: "object",
|
|
42
|
+
properties: { words: { type: "integer" } },
|
|
43
|
+
required: ["words"]
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "explode",
|
|
48
|
+
description: "Always reports a tool-level failure",
|
|
49
|
+
inputSchema: { type: "object", properties: {} }
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
const send = (message) => process.stdout.write(`${JSON.stringify(message)}\n`)
|
|
54
|
+
|
|
55
|
+
createInterface({ input: process.stdin }).on("line", (line) => {
|
|
56
|
+
const request = JSON.parse(line)
|
|
57
|
+
if (request.method === "initialize") {
|
|
58
|
+
send({
|
|
59
|
+
jsonrpc: "2.0",
|
|
60
|
+
id: request.id,
|
|
61
|
+
result: {
|
|
62
|
+
protocolVersion: "2025-06-18",
|
|
63
|
+
capabilities: { tools: {} },
|
|
64
|
+
serverInfo: { name: "quickstart", version: "1.0.0" }
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
if (request.method === "tools/list") {
|
|
70
|
+
send({ jsonrpc: "2.0", id: request.id, result: { tools } })
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
if (request.method === "tools/call") {
|
|
74
|
+
if (request.params.name === "explode") {
|
|
75
|
+
send({
|
|
76
|
+
jsonrpc: "2.0",
|
|
77
|
+
id: request.id,
|
|
78
|
+
result: { content: [{ type: "text", text: "the tool refused" }], isError: true }
|
|
79
|
+
})
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
const text = String(request.params.arguments.text ?? "")
|
|
83
|
+
const words = text.split(/\s+/u).filter((word) => word.length > 0).length
|
|
84
|
+
send({
|
|
85
|
+
jsonrpc: "2.0",
|
|
86
|
+
id: request.id,
|
|
87
|
+
result: {
|
|
88
|
+
content: [{ type: "text", text: String(words) }],
|
|
89
|
+
structuredContent: { words },
|
|
90
|
+
isError: false
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Two details matter later. The `initialize` result declares a `tools`
|
|
98
|
+
capability, which this client requires: a server that serves no tools is
|
|
99
|
+
refused during the handshake. And `explode` answers a successful JSON-RPC reply
|
|
100
|
+
carrying `isError: true`, which is MCP's way of saying "the tool ran and
|
|
101
|
+
reported a problem", not "the call failed".
|
|
102
|
+
|
|
103
|
+
## Connect and call
|
|
104
|
+
|
|
105
|
+
Create `quickstart.ts`. `McpClient.connect` spawns the server, completes the
|
|
106
|
+
handshake, and fetches the tool catalog once, up front:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import { NodeServices } from "@effect/platform-node"
|
|
110
|
+
import * as McpClient from "@smthrs/mcp/McpClient"
|
|
111
|
+
import * as McpFlows from "@smthrs/mcp/McpFlows"
|
|
112
|
+
import { Effect } from "effect"
|
|
113
|
+
|
|
114
|
+
const program = Effect.scoped(Effect.gen(function*() {
|
|
115
|
+
const client = yield* McpClient.connect({
|
|
116
|
+
server: "quickstart",
|
|
117
|
+
command: process.execPath,
|
|
118
|
+
args: ["quickstart-server.mjs"]
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const counted = yield* client.callTool("word_count", { text: "durable flows release notes" })
|
|
122
|
+
const refused = yield* client.callTool("explode", {})
|
|
123
|
+
|
|
124
|
+
const source = McpFlows.mcp(client)
|
|
125
|
+
const bindings = yield* source.bindings()
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
tools: client.tools.map((tool) => tool.name),
|
|
129
|
+
counted,
|
|
130
|
+
refused,
|
|
131
|
+
sourceName: source.name,
|
|
132
|
+
flowNames: bindings.map((binding) => binding.descriptor.name)
|
|
133
|
+
}
|
|
134
|
+
}))
|
|
135
|
+
|
|
136
|
+
console.log(await Effect.runPromise(Effect.provide(program, NodeServices.layer)))
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`Effect.scoped` is what owns the subprocess. When the scope closes, the server
|
|
140
|
+
process is torn down and every request still outstanding fails with
|
|
141
|
+
`connection_closed` rather than hanging.
|
|
142
|
+
|
|
143
|
+
## Run it
|
|
144
|
+
|
|
145
|
+
Run the file with a TypeScript runner:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
npx tsx quickstart.ts
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
```text
|
|
152
|
+
{
|
|
153
|
+
tools: [ 'word_count', 'explode' ],
|
|
154
|
+
counted: {
|
|
155
|
+
content: [ { type: 'text', text: '4' } ],
|
|
156
|
+
isError: false,
|
|
157
|
+
structuredContent: { words: 4 }
|
|
158
|
+
},
|
|
159
|
+
refused: {
|
|
160
|
+
content: [ { type: 'text', text: 'the tool refused' } ],
|
|
161
|
+
isError: true,
|
|
162
|
+
structuredContent: undefined
|
|
163
|
+
},
|
|
164
|
+
sourceName: 'mcp/quickstart',
|
|
165
|
+
flowNames: [ 'mcp/quickstart/word_count', 'mcp/quickstart/explode' ]
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## What just happened
|
|
170
|
+
|
|
171
|
+
`connect` spawned the process, negotiated `2025-06-18`, sent
|
|
172
|
+
`notifications/initialized`, and walked `tools/list` to the end of its cursor
|
|
173
|
+
before returning. `client.tools` is that catalog, and it is a snapshot: this
|
|
174
|
+
client never re-polls it.
|
|
175
|
+
|
|
176
|
+
`callTool("explode", {})` **succeeded**. Its `isError: true` came back in the
|
|
177
|
+
success channel, because a tool that runs and reports a problem is data. Only a
|
|
178
|
+
failure of the session itself, a server that will not start, a closed pipe, a
|
|
179
|
+
reply this client cannot parse, becomes an `McpError`. See
|
|
180
|
+
[Handle a failed tool call](./guides/handle-a-failed-tool-call.md).
|
|
181
|
+
|
|
182
|
+
`word_count` declared an `outputSchema`, so its `structuredContent` was checked
|
|
183
|
+
against that schema before it was returned. `explode` declared none, so nothing
|
|
184
|
+
was checked. See
|
|
185
|
+
[Validate structured output](./guides/validate-structured-output.md).
|
|
186
|
+
|
|
187
|
+
`McpFlows.mcp(client)` produced a `FlowBinding.Source` named `mcp/quickstart`
|
|
188
|
+
holding one binding per tool. Each binding's descriptor carries the server's own
|
|
189
|
+
`inputSchema` as its input document, so a model reading the catalog sees
|
|
190
|
+
`{ type: "object", properties: { text: { type: "string" } }, required: ["text"] }`
|
|
191
|
+
rather than this package's permissive record type.
|
|
192
|
+
|
|
193
|
+
## Next steps
|
|
194
|
+
|
|
195
|
+
- [A remote tool as a flow](./concepts/tools-as-flows.md): what the projection
|
|
196
|
+
produces, and why nothing downstream knows the flow is remote.
|
|
197
|
+
- [Grant authority to MCP tools](./guides/grant-authority-to-mcp-tools.md): the
|
|
198
|
+
one step between a projected source and a cell that can actually call it.
|
|
199
|
+
- [Select the tools a run sees](./guides/select-the-tools-a-run-sees.md): keep a
|
|
200
|
+
fifty-tool server out of the model's context window.
|