@dsh-cc/mcp-client 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/LICENSE +201 -0
- package/README.i18n.yaml +6 -0
- package/README.md +149 -0
- package/README.zh.md +150 -0
- package/lib/auth.d.ts +66 -0
- package/lib/auth.d.ts.map +1 -0
- package/lib/auth.js +121 -0
- package/lib/auth.js.map +1 -0
- package/lib/connection.d.ts +98 -0
- package/lib/connection.d.ts.map +1 -0
- package/lib/connection.js +409 -0
- package/lib/connection.js.map +1 -0
- package/lib/defer.d.ts +39 -0
- package/lib/defer.d.ts.map +1 -0
- package/lib/defer.js +40 -0
- package/lib/defer.js.map +1 -0
- package/lib/index.d.ts +129 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +198 -0
- package/lib/index.js.map +1 -0
- package/lib/invariant.d.ts +16 -0
- package/lib/invariant.d.ts.map +1 -0
- package/lib/invariant.js +22 -0
- package/lib/invariant.js.map +1 -0
- package/lib/prompts.d.ts +44 -0
- package/lib/prompts.d.ts.map +1 -0
- package/lib/prompts.js +166 -0
- package/lib/prompts.js.map +1 -0
- package/lib/registry.d.ts +94 -0
- package/lib/registry.d.ts.map +1 -0
- package/lib/registry.js +101 -0
- package/lib/registry.js.map +1 -0
- package/lib/resources.d.ts +42 -0
- package/lib/resources.d.ts.map +1 -0
- package/lib/resources.js +136 -0
- package/lib/resources.js.map +1 -0
- package/lib/stdio-stderr.d.ts +66 -0
- package/lib/stdio-stderr.d.ts.map +1 -0
- package/lib/stdio-stderr.js +187 -0
- package/lib/stdio-stderr.js.map +1 -0
- package/lib/tools.d.ts +161 -0
- package/lib/tools.d.ts.map +1 -0
- package/lib/tools.js +373 -0
- package/lib/tools.js.map +1 -0
- package/lib/transport.d.ts +50 -0
- package/lib/transport.d.ts.map +1 -0
- package/lib/transport.js +79 -0
- package/lib/transport.js.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture stdio MCP server stderr so it cannot inherit the parent TTY.
|
|
3
|
+
* SDK default is `stderr: 'inherit'`; piping is mandatory for a TUI host.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import { createWriteStream, mkdirSync, openSync, renameSync, statSync } from 'node:fs';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
10
|
+
/** Ring capacity for the in-memory tail used in connection-failure warns. */
|
|
11
|
+
export const STDERR_TAIL_CAPACITY = 4 * 1024;
|
|
12
|
+
/** Max characters of that tail embedded in a single-line `ctx.logger.warn`. */
|
|
13
|
+
export const STDERR_WARN_CAPACITY = 1024;
|
|
14
|
+
/** Default size cap for the stdio stderr log; ONE backup generation is kept. */
|
|
15
|
+
export const STDIO_LOG_MAX_BYTES = 4 * 1024 * 1024;
|
|
16
|
+
const tails = new WeakMap();
|
|
17
|
+
/**
|
|
18
|
+
* Append-only ring of the newest `capacity` characters. Used so a noisy
|
|
19
|
+
* server cannot grow memory without bound while still leaving a crash banner
|
|
20
|
+
* for the supervisor's warn line.
|
|
21
|
+
*/
|
|
22
|
+
export class BoundedTail {
|
|
23
|
+
capacity;
|
|
24
|
+
buffer = '';
|
|
25
|
+
constructor(capacity) {
|
|
26
|
+
this.capacity = capacity;
|
|
27
|
+
}
|
|
28
|
+
push(chunk) {
|
|
29
|
+
this.buffer += chunk;
|
|
30
|
+
if (this.buffer.length > this.capacity) {
|
|
31
|
+
this.buffer = this.buffer.slice(this.buffer.length - this.capacity);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
snapshot() {
|
|
35
|
+
return this.buffer;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Default directory for captured stdio stderr.
|
|
40
|
+
* `$DSH_HOME/mcp-logs`, falling back to `~/.dsh/mcp-logs`.
|
|
41
|
+
*/
|
|
42
|
+
function defaultStdioLogDir() {
|
|
43
|
+
return join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'mcp-logs');
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Last ≤1 KB of a captured tail, newlines collapsed, for a one-line logger.
|
|
47
|
+
* @returns `undefined` when the tail is empty so callers can omit the suffix.
|
|
48
|
+
*/
|
|
49
|
+
export function formatStdioStderrForWarn(tail) {
|
|
50
|
+
const collapsed = tail.trim().replace(/\r?\n/g, '⏎');
|
|
51
|
+
if (collapsed.length === 0)
|
|
52
|
+
return undefined;
|
|
53
|
+
return collapsed.length > STDERR_WARN_CAPACITY
|
|
54
|
+
? collapsed.slice(collapsed.length - STDERR_WARN_CAPACITY)
|
|
55
|
+
: collapsed;
|
|
56
|
+
}
|
|
57
|
+
/** Snapshot of the ring attached to `transport`, or `''` when none. */
|
|
58
|
+
export function stdioStderrTail(transport) {
|
|
59
|
+
return tails.get(transport)?.snapshot() ?? '';
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Pipe `transport.stderr`, drain into a 4 KB ring, and append raw bytes to
|
|
63
|
+
* `$DSH_HOME/mcp-logs/<serverName>.log` (or `logDir` when given), rotating at
|
|
64
|
+
* `maxBytes` (default {@link STDIO_LOG_MAX_BYTES}; `maxBytes <= 0` disables)
|
|
65
|
+
* into a single `<serverName>.log.1` backup generation, both at open time and
|
|
66
|
+
* mid-stream.
|
|
67
|
+
*
|
|
68
|
+
* Must run before `start()`: SDK 1.29 creates the PassThrough in the
|
|
69
|
+
* constructor when `stderr: 'pipe'`. An undrained pipe back-pressures the
|
|
70
|
+
* child until it blocks in `write(2)`.
|
|
71
|
+
*
|
|
72
|
+
* No-ops when `stderr` is missing so `vi.fn()` SDK mocks stay constructible.
|
|
73
|
+
*
|
|
74
|
+
* ponytail: rotation is size-based with one backup generation (the previous
|
|
75
|
+
* `.log.1` is discarded on each rotation); concurrent dsh-cc sessions sharing
|
|
76
|
+
* a serverName may interleave (session header marks each generation);
|
|
77
|
+
* WriteStream buffer grows if the disk stalls — log must never block MCP.
|
|
78
|
+
* Documented rotation edges: (a) bytes buffered in the old stream at rotation
|
|
79
|
+
* time flush into `.log.1` after the rename — a crash between rename and
|
|
80
|
+
* flush loses at most one stream buffer; (b) another session holding the
|
|
81
|
+
* renamed `.log.1` writes into an anonymous inode whose bytes vanish on
|
|
82
|
+
* close — worst-case extra disk ≈ maxBytes × live sessions. A failed
|
|
83
|
+
* rotation rename (e.g. `.log.1` is a directory) sets `rotationDisabled` so
|
|
84
|
+
* appending cannot become a rename-retry storm per chunk; appending then
|
|
85
|
+
* grows unbounded, which is exactly the pre-rotation worst case.
|
|
86
|
+
*
|
|
87
|
+
* INVARIANT: all rotation fs ops (`statSync`/`renameSync`/`createWriteStream`)
|
|
88
|
+
* are SYNCHRONOUS and run inside the `data` handler / open path. PassThrough
|
|
89
|
+
* `data` events cannot interleave, and the `end` handler cannot fire
|
|
90
|
+
* mid-rotation; async fs here would reintroduce real races.
|
|
91
|
+
*/
|
|
92
|
+
export function attachStdioStderrDrain(transport, serverName, logDir, maxBytes = STDIO_LOG_MAX_BYTES) {
|
|
93
|
+
const stream = transport.stderr;
|
|
94
|
+
if (stream === null || stream === undefined)
|
|
95
|
+
return;
|
|
96
|
+
const tail = new BoundedTail(STDERR_TAIL_CAPACITY);
|
|
97
|
+
tails.set(transport, tail);
|
|
98
|
+
const dir = logDir ?? defaultStdioLogDir();
|
|
99
|
+
const path = join(dir, `${serverName}.log`);
|
|
100
|
+
let rotationDisabled = maxBytes <= 0;
|
|
101
|
+
let file;
|
|
102
|
+
// Byte tally for the currently open generation, reset to the just-opened
|
|
103
|
+
// file's size on every open (0 after a rotation, pre-existing size
|
|
104
|
+
// otherwise). The `--- dsh-cc pid … ---` session header bytes are
|
|
105
|
+
// deliberately NOT counted: the cap governs captured server output, not
|
|
106
|
+
// our bookkeeping lines.
|
|
107
|
+
let tally = 0;
|
|
108
|
+
const rotationActive = () => !rotationDisabled;
|
|
109
|
+
const open = () => {
|
|
110
|
+
file = openLogFile(path, rotationActive() ? maxBytes : 0, (seed) => { tally = seed; });
|
|
111
|
+
};
|
|
112
|
+
stream.on('data', (chunk) => {
|
|
113
|
+
const bytes = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
|
|
114
|
+
tail.push(bytes.toString('utf8'));
|
|
115
|
+
if (file === undefined)
|
|
116
|
+
open(); // lazy: a silent server leaves no file
|
|
117
|
+
if (file === undefined)
|
|
118
|
+
return;
|
|
119
|
+
file.write(bytes);
|
|
120
|
+
tally += bytes.length;
|
|
121
|
+
if (!rotationDisabled && tally >= maxBytes) {
|
|
122
|
+
let renamed = false;
|
|
123
|
+
try {
|
|
124
|
+
// Rename WHILE the old stream is open: POSIX keeps the open fd on the
|
|
125
|
+
// inode, so buffered bytes flush into `.log.1` — no loss. The chunk
|
|
126
|
+
// that crossed the boundary stays in the old file; never split it.
|
|
127
|
+
renameSync(path, `${path}.1`);
|
|
128
|
+
renamed = true;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// Unrenamable backup path (EISDIR/EPERM/…): stop trying, or every
|
|
132
|
+
// subsequent chunk retriggers the same doomed rename.
|
|
133
|
+
rotationDisabled = true;
|
|
134
|
+
}
|
|
135
|
+
if (renamed) {
|
|
136
|
+
file.end();
|
|
137
|
+
open();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
// SDK close() does not destroy the PassThrough; the child's stderr `end`
|
|
142
|
+
// does. Keep the data listener through SIGTERM grace so the last crash
|
|
143
|
+
// line lands in both the ring and the file.
|
|
144
|
+
stream.on('end', () => { file?.end(); file = undefined; });
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Open (lazily) the generation log file: rotate an oversized existing file to
|
|
148
|
+
* `<path>.1` first, seed the caller's byte tally from the file's size, then
|
|
149
|
+
* append a fresh session header. Never throws.
|
|
150
|
+
*/
|
|
151
|
+
function openLogFile(path, maxBytes, seedTally) {
|
|
152
|
+
try {
|
|
153
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
154
|
+
if (maxBytes > 0 && fileSizeOrZero(path) >= maxBytes) {
|
|
155
|
+
try {
|
|
156
|
+
renameSync(path, `${path}.1`);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// Leave the oversized file in place and keep appending; the mid-stream
|
|
160
|
+
// path disables rotation on the same condition via its own flag.
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
seedTally(maxBytes > 0 ? fileSizeOrZero(path) : 0);
|
|
164
|
+
// openSync binds the fd to the inode NOW: a mid-stream renameSync then
|
|
165
|
+
// keeps this fd on the renamed `.1`, so bytes queued before the rename
|
|
166
|
+
// still flush into the old generation. A lazily-opened WriteStream would
|
|
167
|
+
// resolve its open AFTER the rename and recreate `path` — splitting one
|
|
168
|
+
// chunk batch across generations.
|
|
169
|
+
const file = createWriteStream(path, { fd: openSync(path, 'a') });
|
|
170
|
+
file.on('error', () => { });
|
|
171
|
+
file.write(`--- dsh-cc pid ${process.pid} ${new Date().toISOString()} ---\n`);
|
|
172
|
+
return file;
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/** File size, or 0 when missing (ENOENT is the common first-run path). */
|
|
179
|
+
function fileSizeOrZero(path) {
|
|
180
|
+
try {
|
|
181
|
+
return statSync(path).size;
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
//# sourceMappingURL=stdio-stderr.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stdio-stderr.js","sourceRoot":"","sources":["../src/stdio-stderr.ts"],"names":[],"mappings":"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"}
|
package/lib/tools.d.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool bridge: discovers MCP tools, registers them on the harness ToolRuntime
|
|
3
|
+
* under deterministic server-qualified public names, and handles re-sync when
|
|
4
|
+
* the server's tool list changes.
|
|
5
|
+
*
|
|
6
|
+
* Naming contract (see the mcp-client Agent Note "Naming invariants"): every MCP tool
|
|
7
|
+
* has the stable identity `(serverName, rawName)`; the model-facing public name
|
|
8
|
+
* is `mcp__<serverName>__<rawName>`, normalized to the DeepSeek function-name
|
|
9
|
+
* constraints. The raw name is only ever sent on the wire (`tools/call`); the
|
|
10
|
+
* public name is never parsed to recover it.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
import type { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
15
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
16
|
+
import type { JsonValue } from '@dsh-cc/tools';
|
|
17
|
+
export { DEFAULT_DEFER_TOOL_THRESHOLD } from './defer.ts';
|
|
18
|
+
/** Resolved options relevant to tool bridging. */
|
|
19
|
+
export interface ToolBridgeOptions {
|
|
20
|
+
/** Whether a registry conflict is contained or rejects this synchronization. */
|
|
21
|
+
registrationFailure: 'contain' | 'throw';
|
|
22
|
+
serverName: string;
|
|
23
|
+
toolCallTimeoutMs: number;
|
|
24
|
+
/**
|
|
25
|
+
* Listed-tool count above which this server's deferrable tools register
|
|
26
|
+
* through `ctx.toolSearch` instead of eagerly. Counts the server's
|
|
27
|
+
* `tools/list` length including alwaysLoad tools; a tool flagged
|
|
28
|
+
* `_meta['anthropic/alwaysLoad']` is still eager even on a deferred server.
|
|
29
|
+
* `0` defers whenever `toolSearch` is present. Default
|
|
30
|
+
* {@link DEFAULT_DEFER_TOOL_THRESHOLD}.
|
|
31
|
+
*/
|
|
32
|
+
deferToolThreshold?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Called before a single mid-session 401 retry. The SDK auto-refreshes an
|
|
35
|
+
* expired token before a request; a mid-session `UnauthorizedError` means the
|
|
36
|
+
* token was revoked, so the provider drops the stored state and re-runs the
|
|
37
|
+
* token flow before the bridge retries the request once.
|
|
38
|
+
*/
|
|
39
|
+
onUnauthorized?: () => Promise<void> | void;
|
|
40
|
+
}
|
|
41
|
+
/** State for one sync generation: the current set of disposers keyed by public name. */
|
|
42
|
+
export type ToolDisposers = Map<string, () => void>;
|
|
43
|
+
/**
|
|
44
|
+
* One registered tool generation plus the identity data used to skip no-op
|
|
45
|
+
* swaps. `fingerprintTools` fingerprints the raw server payload; when a
|
|
46
|
+
* re-sync produces the same fingerprint on the same client generation, the
|
|
47
|
+
* live registrations are kept as-is so request prefixes stay stable.
|
|
48
|
+
*/
|
|
49
|
+
export interface ToolGeneration {
|
|
50
|
+
/** Live registrations owned by this generation, keyed by public name. */
|
|
51
|
+
disposers: ToolDisposers;
|
|
52
|
+
/**
|
|
53
|
+
* Fingerprint of the raw server payload that produced this generation.
|
|
54
|
+
* `undefined` when nothing is registered (initial state or rolled-back
|
|
55
|
+
* registration), which forces the next sync to attempt a real swap.
|
|
56
|
+
*/
|
|
57
|
+
fingerprint: string | undefined;
|
|
58
|
+
/** The client generation the payload was fetched from; a new client forces a swap. */
|
|
59
|
+
client: Client | undefined;
|
|
60
|
+
/** Listed tools registered eagerly (visible in the model-facing schema). */
|
|
61
|
+
eagerCount: number;
|
|
62
|
+
/** Listed tools registered deferred (searchable, hidden until activated). */
|
|
63
|
+
deferredCount: number;
|
|
64
|
+
}
|
|
65
|
+
/** The generation representing "nothing registered yet" (or a rolled-back swap). */
|
|
66
|
+
export declare function emptyToolGeneration(): ToolGeneration;
|
|
67
|
+
/**
|
|
68
|
+
* Fingerprint the raw tools/list payload for swap short-circuiting.
|
|
69
|
+
*
|
|
70
|
+
* Entries are ordered by public name, then stable-stringified (recursive key
|
|
71
|
+
* sort, array order preserved) and hashed. The fingerprint covers every field
|
|
72
|
+
* of the raw entries — including `execution.taskSupport`, which
|
|
73
|
+
* `createExecutor` bakes into the executor's semantics, and `outputSchema`,
|
|
74
|
+
* which shapes the registered output schema — so any server-side semantic
|
|
75
|
+
* change forces a swap. Executor closures are not compared (functions are not
|
|
76
|
+
* serializable); identical fingerprints on one client generation imply the
|
|
77
|
+
* rebuilt executors would be behaviorally identical.
|
|
78
|
+
*
|
|
79
|
+
* @param serverName - Namespace used to derive each entry's public name for ordering.
|
|
80
|
+
* @param tools - Raw entries exactly as returned by the server's `tools/list`.
|
|
81
|
+
* @returns A hex digest that is equal precisely when the payload is semantically unchanged.
|
|
82
|
+
*/
|
|
83
|
+
export declare function fingerprintTools(serverName: string, tools: readonly unknown[]): string;
|
|
84
|
+
/** Canonical MCP result exposed to Code Mode without discarding protocol blocks. */
|
|
85
|
+
export type McpResult<Structured extends JsonValue = JsonValue> = {
|
|
86
|
+
content: JsonValue[];
|
|
87
|
+
structuredContent?: Structured;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Derive the model-facing public name for one MCP tool.
|
|
91
|
+
*
|
|
92
|
+
* Deterministic pure function of `(serverName, rawName)`: the clean case is
|
|
93
|
+
* `mcp__<serverName>__<rawName>` verbatim. When character replacement or
|
|
94
|
+
* truncation to the DeepSeek function-name contract (64 chars,
|
|
95
|
+
* `[A-Za-z0-9_-]`) changes the name, a 12-hex-char SHA-256 hash of the
|
|
96
|
+
* identity is appended so distinct MCP identities never collapse into the
|
|
97
|
+
* same public name.
|
|
98
|
+
*
|
|
99
|
+
* @param serverName - Stable local namespace from plugin config.
|
|
100
|
+
* @param rawName - The MCP server's own tool name.
|
|
101
|
+
* @returns The globally unique, model-facing ToolRuntime name.
|
|
102
|
+
*/
|
|
103
|
+
export declare function publicToolName(serverName: string, rawName: string): string;
|
|
104
|
+
/**
|
|
105
|
+
* Sync the MCP server's tool list into the harness ToolRuntime.
|
|
106
|
+
*
|
|
107
|
+
* Two phases keep the swap safe:
|
|
108
|
+
*
|
|
109
|
+
* 1. Fetch: drain uncached `tools/list` pagination and build the full next
|
|
110
|
+
* generation of `ToolDefinition`s under public names. Any failure here
|
|
111
|
+
* (network error, duplicate raw name in the server's list) rejects and
|
|
112
|
+
* leaves the previous generation registered untouched.
|
|
113
|
+
* 2. Swap: dispose the previous generation, publish the new one. A registry
|
|
114
|
+
* conflict here can only mean a foreign registration squats on this
|
|
115
|
+
* server's `mcp__<serverName>__` namespace — the partial generation is
|
|
116
|
+
* rolled back (zero tools from this server) and logged. Initial strict
|
|
117
|
+
* synchronization may propagate the conflict so its parent transaction
|
|
118
|
+
* rejects; ordinary clients and later re-syncs return an empty generation.
|
|
119
|
+
* Deferred publishing detects the same squat up front (`ctx.tools.get`)
|
|
120
|
+
* because `registerDeferred` only reserves the name.
|
|
121
|
+
*
|
|
122
|
+
* When the `ctx.toolSearch` seam is mounted and the server lists at least
|
|
123
|
+
* `deferToolThreshold` tools (default {@link DEFAULT_DEFER_TOOL_THRESHOLD}),
|
|
124
|
+
* each deferrable tool registers through `registerDeferred` instead: its
|
|
125
|
+
* definition stays out of the model-visible schema until a ToolSearch hit
|
|
126
|
+
* activates it. Tools flagged `_meta['anthropic/alwaysLoad']` register eagerly
|
|
127
|
+
* even on a deferred server. Without the seam the swap is eager at any
|
|
128
|
+
* threshold.
|
|
129
|
+
*
|
|
130
|
+
* Between the phases, a fingerprint of the raw payload decides whether the
|
|
131
|
+
* swap is needed at all: when the payload is semantically unchanged (key
|
|
132
|
+
* order may drift; content may not) AND `previous` was produced by the same
|
|
133
|
+
* client generation, the live registrations are kept and `previous` is
|
|
134
|
+
* returned unchanged — dispose+register churn (and the request-prefix churn
|
|
135
|
+
* it risks) is skipped. A new client generation always forces a real swap,
|
|
136
|
+
* so reconnects never reuse the previous generation's registrations.
|
|
137
|
+
*
|
|
138
|
+
* @param client - Connected MCP Client instance used to list and call tools.
|
|
139
|
+
* @param ctx - Cordis context providing the `tools` service for registration.
|
|
140
|
+
* @param opts - Bridge options: server namespace and per-call timeout.
|
|
141
|
+
* @param previous - The prior sync generation; its registrations are disposed
|
|
142
|
+
* during the swap phase (only after the fetch phase succeeded and the
|
|
143
|
+
* fingerprint check found a real change).
|
|
144
|
+
* @returns The live generation — `previous` itself on a fingerprint hit,
|
|
145
|
+
* otherwise the newly registered one.
|
|
146
|
+
*/
|
|
147
|
+
export declare function syncTools(client: Client, ctx: Context, opts: ToolBridgeOptions, previous: ToolGeneration): Promise<ToolGeneration>;
|
|
148
|
+
/**
|
|
149
|
+
* Run an MCP request, retrying once on a mid-session `UnauthorizedError`.
|
|
150
|
+
* Between the original attempt and the retry, `onUnauthorized` runs to drop
|
|
151
|
+
* stale OAuth state and re-establish a token. Only a single retry is attempted
|
|
152
|
+
* (the spec's "401 自动重试一次"); a second failure propagates to the caller.
|
|
153
|
+
*
|
|
154
|
+
* @param request - the MCP request to attempt.
|
|
155
|
+
* @param onUnauthorized - re-auth hook run before the single retry.
|
|
156
|
+
* @returns the request result.
|
|
157
|
+
*/
|
|
158
|
+
export declare function retryUnauthorizedOnce<T>(request: () => Promise<T>, onUnauthorized?: () => Promise<void> | void): Promise<T>;
|
|
159
|
+
/** Whether a thrown error signals an expired/revoked OAuth session. */
|
|
160
|
+
export declare function isUnauthorized(error: unknown): boolean;
|
|
161
|
+
//# sourceMappingURL=tools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAA;AAIvE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAGlD,OAAO,KAAK,EAAkB,SAAS,EAAE,MAAM,eAAe,CAAA;AAG9D,OAAO,EAAE,4BAA4B,EAAE,MAAM,YAAY,CAAA;AAEzD,kDAAkD;AAClD,MAAM,WAAW,iBAAiB;IAChC,gFAAgF;IAChF,mBAAmB,EAAE,SAAS,GAAG,OAAO,CAAA;IACxC,UAAU,EAAE,MAAM,CAAA;IAClB,iBAAiB,EAAE,MAAM,CAAA;IACzB;;;;;;;OAOG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAA;IAC3B;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;CAC5C;AAED,wFAAwF;AACxF,MAAM,MAAM,aAAa,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,IAAI,CAAC,CAAA;AAEnD;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,yEAAyE;IACzE,SAAS,EAAE,aAAa,CAAA;IACxB;;;;OAIG;IACH,WAAW,EAAE,MAAM,GAAG,SAAS,CAAA;IAC/B,sFAAsF;IACtF,MAAM,EAAE,MAAM,GAAG,SAAS,CAAA;IAC1B,4EAA4E;IAC5E,UAAU,EAAE,MAAM,CAAA;IAClB,6EAA6E;IAC7E,aAAa,EAAE,MAAM,CAAA;CACtB;AAED,oFAAoF;AACpF,wBAAgB,mBAAmB,IAAI,cAAc,CAEpD;AAgBD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,OAAO,EAAE,GAAG,MAAM,CAWtF;AAED,oFAAoF;AACpF,MAAM,MAAM,SAAS,CAAC,UAAU,SAAS,SAAS,GAAG,SAAS,IAAI;IAChE,OAAO,EAAE,SAAS,EAAE,CAAA;IACpB,iBAAiB,CAAC,EAAE,UAAU,CAAA;CAC/B,CAAA;AA2CD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAM1E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0CG;AACH,wBAAsB,SAAS,CAC7B,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,OAAO,EACZ,IAAI,EAAE,iBAAiB,EACvB,QAAQ,EAAE,cAAc,GACvB,OAAO,CAAC,cAAc,CAAC,CAuEzB;AA4CD;;;;;;;;;GASG;AACH,wBAAsB,qBAAqB,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EAAE,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,CAQjI;AAED,uEAAuE;AACvE,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAGtD"}
|