pi-code 1.0.25 → 1.0.26
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/extensions/internal/plugins.ts +3 -1
- package/extensions/mcp/config.ts +38 -12
- package/extensions/mcp/index.ts +28 -11
- package/extensions/mcp/mapping.ts +12 -13
- package/extensions/mcp/transport.ts +46 -15
- package/package.json +1 -1
|
@@ -180,7 +180,9 @@ function resolvePlugin(home: string, cacheDir: string, marketplace: string, plug
|
|
|
180
180
|
const root = path.join(cacheDir, marketplace, pluginDir, version)
|
|
181
181
|
const manifest = readJson(path.join(root, '.claude-plugin', 'plugin.json'))
|
|
182
182
|
const name = typeof manifest.name === 'string' && manifest.name.length > 0 ? manifest.name : pluginDir
|
|
183
|
-
|
|
183
|
+
// Claude: "{id} is the plugin identifier with characters outside a-z, A-Z, 0-9,
|
|
184
|
+
// _, and - replaced by -", one dash per character, underscores kept.
|
|
185
|
+
const id = qualified.replace(/[^A-Za-z0-9_-]/g, '-')
|
|
184
186
|
const userConfig = configs[qualified] ?? configs[pluginDir] ?? configs[name]
|
|
185
187
|
return { name, root, dataDir: path.join(claudeConfigDir(home), 'plugins', 'data', id), manifest, ...(userConfig ? { userConfig } : {}) }
|
|
186
188
|
}
|
package/extensions/mcp/config.ts
CHANGED
|
@@ -99,13 +99,34 @@ export function loadConfigFrom(files: string[]): Record<string, ServerConfig> {
|
|
|
99
99
|
*/
|
|
100
100
|
export function loadUserScope(home: string, cwd: string): Record<string, ServerConfig> {
|
|
101
101
|
const servers = loadConfigFrom(userConfigPaths(home))
|
|
102
|
+
Object.assign(servers, projectRecord(home, cwd).mcpServers ?? {})
|
|
103
|
+
return servers
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** The per-project record for `cwd` in ~/.claude.json, or an empty one when the file
|
|
107
|
+
* is missing, invalid, or has no entry for this project. */
|
|
108
|
+
function projectRecord(home: string, cwd: string): { mcpServers?: Record<string, ServerConfig>; disabledMcpServers?: unknown } {
|
|
102
109
|
try {
|
|
103
110
|
const claudeJson = JSON.parse(fs.readFileSync(claudeJsonPath(home), 'utf-8'))
|
|
104
|
-
|
|
111
|
+
return claudeJson.projects?.[cwd] ?? {}
|
|
105
112
|
} catch {
|
|
106
|
-
|
|
113
|
+
return {}
|
|
107
114
|
}
|
|
108
|
-
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Names the local scope defines for this project. Claude's precedence is local over
|
|
118
|
+
* project over user, so a local name must also outrank a project .mcp.json entry. */
|
|
119
|
+
export function localScopeServerNames(home: string, cwd: string): Set<string> {
|
|
120
|
+
return new Set(Object.keys(projectRecord(home, cwd).mcpServers ?? {}))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The per-project `disabledMcpServers` toggle list from ~/.claude.json: Claude's /mcp
|
|
124
|
+
* panel records a server toggled off here (an opt-out list for user-configured and
|
|
125
|
+
* plugin servers) and does not connect to it. The `enabledMcpServers` opt-in list
|
|
126
|
+
* covers only default-off built-in servers, which pi-code has none of. */
|
|
127
|
+
export function disabledServerNames(home: string, cwd: string): Set<string> {
|
|
128
|
+
const listed = projectRecord(home, cwd).disabledMcpServers
|
|
129
|
+
return new Set(Array.isArray(listed) ? listed.filter((entry): entry is string => typeof entry === 'string') : [])
|
|
109
130
|
}
|
|
110
131
|
|
|
111
132
|
/** The mcpServers one plugin declares, parsed WITHOUT substitution: an inline map on
|
|
@@ -115,18 +136,22 @@ export function loadUserScope(home: string, cwd: string): Record<string, ServerC
|
|
|
115
136
|
* the parse and headersHelper can be shielded. */
|
|
116
137
|
function rawPluginServerEntries(plugin: InstalledPlugin): Record<string, unknown> {
|
|
117
138
|
const declared = plugin.manifest.mcpServers
|
|
118
|
-
// An inline map of name -> config
|
|
119
|
-
//
|
|
139
|
+
// An inline map of name -> config. The manifest field is string|array|object per
|
|
140
|
+
// Claude's plugin reference; an array lists config file paths, merged in order.
|
|
120
141
|
if (declared !== null && typeof declared === 'object' && !Array.isArray(declared)) {
|
|
121
142
|
return { ...(declared as Record<string, unknown>) }
|
|
122
143
|
}
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
144
|
+
const paths = Array.isArray(declared) ? declared.filter((entry): entry is string => typeof entry === 'string') : [typeof declared === 'string' ? declared : '.mcp.json']
|
|
145
|
+
const servers: Record<string, unknown> = {}
|
|
146
|
+
for (const entry of paths) {
|
|
147
|
+
try {
|
|
148
|
+
const parsed = JSON.parse(fs.readFileSync(path.resolve(plugin.root, entry), 'utf-8'))
|
|
149
|
+
Object.assign(servers, parsed.mcpServers ?? {})
|
|
150
|
+
} catch {
|
|
151
|
+
// Malformed or missing JSON contributes no entries.
|
|
152
|
+
}
|
|
129
153
|
}
|
|
154
|
+
return servers
|
|
130
155
|
}
|
|
131
156
|
|
|
132
157
|
/** Every string in the value mapped through `substitute`, arrays and objects walked. */
|
|
@@ -169,7 +194,8 @@ function substitutePathPluginVars(text: string, plugin: InstalledPlugin): string
|
|
|
169
194
|
* tools alias as mcp__plugin_<plugin>_<server>__<tool> for hook matchers, as
|
|
170
195
|
* Claude scopes them. */
|
|
171
196
|
export function loadPluginServers(plugins: InstalledPlugin[], projectDir?: string): Record<string, ServerConfig> {
|
|
172
|
-
|
|
197
|
+
// Claude keeps hyphens in the alias; only characters outside A-Za-z0-9_- fold to _.
|
|
198
|
+
const fold = (name: string): string => name.replace(/[^A-Za-z0-9_-]/g, '_')
|
|
173
199
|
const servers: Record<string, ServerConfig> = {}
|
|
174
200
|
for (const plugin of plugins) {
|
|
175
201
|
for (const [name, config] of Object.entries(rawPluginServerEntries(plugin))) {
|
package/extensions/mcp/index.ts
CHANGED
|
@@ -43,11 +43,11 @@ import { installedPlugins } from '../internal/plugins.js'
|
|
|
43
43
|
import { isProjectApproved, isProjectApprovedSilently } from '../internal/project-approval.js'
|
|
44
44
|
import { repoRoot } from '../internal/project-root.js'
|
|
45
45
|
import { claudeSettingsChain } from '../internal/settings-chain.js'
|
|
46
|
-
import { loadConfigFrom, loadPluginServers, loadUserScope, projectConfigPaths, type ServerConfig, warnOnTypelessUrl } from './config.js'
|
|
46
|
+
import { disabledServerNames, loadConfigFrom, loadPluginServers, loadUserScope, localScopeServerNames, projectConfigPaths, type ServerConfig, warnOnTypelessUrl } from './config.js'
|
|
47
47
|
import { collectServerResourceEntries, listAllPrompts, listAllTools, type McpToolInfo, resourceServerFilter } from './listing.js'
|
|
48
48
|
import { formatPromptCommandName, formatToolName, type McpContentBlock, type McpPromptInfo, mapContent, mapPromptArguments, normalizeSchema, promptMessageContent } from './mapping.js'
|
|
49
49
|
import { applyServerPolicy, loadManagedMcpServers, type McpPolicy, mcpAllowDeny, projectServerPolicy, splitByPolicy } from './policy.js'
|
|
50
|
-
import { type AuthUi, callRequestOptions, callTimeoutMs, connect, connectTimeoutMs, withTimeout } from './transport.js'
|
|
50
|
+
import { type AuthUi, callRequestOptions, callTimeoutMs, connect, connectTimeoutMs, type ServerCallTuning, serverCallTuning, withTimeout } from './transport.js'
|
|
51
51
|
|
|
52
52
|
export { managedSettingsPath, setManagedSettingsPath } from '../internal/managed-settings.js'
|
|
53
53
|
// Re-exports for consumers: the module split keeps the extension's public surface
|
|
@@ -84,11 +84,19 @@ function authUiFor(ctx: ExtensionContext): AuthUi | undefined {
|
|
|
84
84
|
export default async function mcpExtension(pi: ExtensionAPI) {
|
|
85
85
|
const clients = new Map<string, Client>()
|
|
86
86
|
const status = new Map<string, { state: string; tools: number }>()
|
|
87
|
+
// Config per server name, kept for call-time timeout tuning: the idle tier follows
|
|
88
|
+
// the transport kind, and a declared per-server timeout governs the wall budget.
|
|
89
|
+
const serverConfigs = new Map<string, ServerConfig>()
|
|
90
|
+
const callTuning = (name: string): ServerCallTuning => {
|
|
91
|
+
const config = serverConfigs.get(name)
|
|
92
|
+
return config ? serverCallTuning(config) : {}
|
|
93
|
+
}
|
|
87
94
|
// Let other extensions (hooks' mcp_tool type) call a connected server's tool.
|
|
88
95
|
setMcpToolCaller(async (server, tool, input) => {
|
|
89
96
|
const client = clients.get(server)
|
|
90
97
|
if (!client) throw new Error(`MCP server "${server}" is not connected`)
|
|
91
|
-
const
|
|
98
|
+
const tuning = callTuning(server)
|
|
99
|
+
const result = await client.callTool({ name: tool, arguments: input }, undefined, callRequestOptions(tuning.serverTimeoutMs ?? callTimeoutMs(), tuning))
|
|
92
100
|
const text = mapContent(result.content as McpContentBlock[], result.structuredContent)
|
|
93
101
|
.filter((part): part is { type: 'text'; text: string } => part.type === 'text')
|
|
94
102
|
.map((part) => part.text)
|
|
@@ -138,9 +146,9 @@ export default async function mcpExtension(pi: ExtensionAPI) {
|
|
|
138
146
|
// own default request timeout is 60s and would otherwise reject first. The outer
|
|
139
147
|
// race uses the wall budget, never the idle window, so a progressing call is not
|
|
140
148
|
// cut off at the idle timeout.
|
|
141
|
-
const
|
|
142
|
-
const wall =
|
|
143
|
-
const result = await withTimeout(current.callTool({ name: tool.name, arguments: params as Record<string, unknown> }, undefined, callRequestOptions(wall)), wall, toolName)
|
|
149
|
+
const tuning = serverCallTuning(config)
|
|
150
|
+
const wall = tuning.serverTimeoutMs ?? callTimeoutMs()
|
|
151
|
+
const result = await withTimeout(current.callTool({ name: tool.name, arguments: params as Record<string, unknown> }, undefined, callRequestOptions(wall, tuning)), wall, toolName)
|
|
144
152
|
const content = mapContent(result.content as McpContentBlock[], result.structuredContent)
|
|
145
153
|
const details: { error?: string } = {}
|
|
146
154
|
if (result.isError) {
|
|
@@ -193,7 +201,7 @@ export default async function mcpExtension(pi: ExtensionAPI) {
|
|
|
193
201
|
const params: { name: string; arguments?: Record<string, string> } = { name: prompt.name }
|
|
194
202
|
if (Object.keys(promptArgs).length > 0) params.arguments = promptArgs
|
|
195
203
|
const wall = callTimeoutMs()
|
|
196
|
-
const result = await withTimeout(current.getPrompt(params, callRequestOptions(wall)), wall, commandName)
|
|
204
|
+
const result = await withTimeout(current.getPrompt(params, callRequestOptions(wall, callTuning(name))), wall, commandName)
|
|
197
205
|
// The prompt drives a turn exactly the way a custom slash command does
|
|
198
206
|
// (see commands.ts), carrying its image blocks through. A prompt that
|
|
199
207
|
// yields no content is reported rather than sent as an empty turn.
|
|
@@ -280,7 +288,7 @@ export default async function mcpExtension(pi: ExtensionAPI) {
|
|
|
280
288
|
const client = clients.get(server)
|
|
281
289
|
if (!client) throw new Error(`MCP server "${server}" is not connected`)
|
|
282
290
|
const wall = callTimeoutMs()
|
|
283
|
-
const result = await withTimeout(client.readResource({ uri }, callRequestOptions(wall)), wall, `read ${uri}`)
|
|
291
|
+
const result = await withTimeout(client.readResource({ uri }, callRequestOptions(wall, callTuning(server))), wall, `read ${uri}`)
|
|
284
292
|
const blocks = (result.contents as Array<{ uri: string; text?: string; blob?: string; mimeType?: string }>).map((entry): McpContentBlock => {
|
|
285
293
|
if (typeof entry.text === 'string') return { type: 'resource', resource: { uri: entry.uri, text: entry.text } }
|
|
286
294
|
if (entry.blob && entry.mimeType?.startsWith('image/')) return { type: 'image', data: entry.blob, mimeType: entry.mimeType }
|
|
@@ -341,6 +349,7 @@ export default async function mcpExtension(pi: ExtensionAPI) {
|
|
|
341
349
|
// Seed in config order before connecting: parallel connects settle in completion
|
|
342
350
|
// order, and /mcp plus the session summary iterate the map's insertion order.
|
|
343
351
|
status.set(name, { state: 'connecting', tools: 0 })
|
|
352
|
+
serverConfigs.set(name, config)
|
|
344
353
|
pending.push([name, config])
|
|
345
354
|
}
|
|
346
355
|
await Promise.all(
|
|
@@ -424,9 +433,12 @@ export default async function mcpExtension(pi: ExtensionAPI) {
|
|
|
424
433
|
* or whose transport dropped, without duplicate-name warnings. */
|
|
425
434
|
async function connectNormalScopes(ctx: ExtensionContext, policy: McpPolicy, authUi?: AuthUi): Promise<void> {
|
|
426
435
|
// Plugin servers merge under the user scope (plugins are user-installed);
|
|
427
|
-
// the user's own entry wins a name clash with a plugin's.
|
|
436
|
+
// the user's own entry wins a name clash with a plugin's. A server toggled off
|
|
437
|
+
// in ~/.claude.json's per-project disabledMcpServers list never connects.
|
|
428
438
|
const pluginServers = loadPluginServers(installedPlugins(os.homedir()), repoRoot(ctx.cwd) ?? ctx.cwd)
|
|
429
|
-
const
|
|
439
|
+
const disabled = disabledServerNames(os.homedir(), ctx.cwd)
|
|
440
|
+
const merged = Object.fromEntries(Object.entries({ ...pluginServers, ...loadUserScope(os.homedir(), ctx.cwd) }).filter(([name]) => !disabled.has(name)))
|
|
441
|
+
const scoped = applyServerPolicy(merged, policy)
|
|
430
442
|
// Claude's precedence is project over user for a duplicate name. A project .mcp.json
|
|
431
443
|
// server only outranks the user's own when it will actually connect (the user already
|
|
432
444
|
// consented to it, or an approved project's), so a merely-present untrusted project
|
|
@@ -436,7 +448,12 @@ export default async function mcpExtension(pi: ExtensionAPI) {
|
|
|
436
448
|
// The stored project decision, read without prompting: consent recorded inside
|
|
437
449
|
// the project only counts once the project itself has been approved.
|
|
438
450
|
const projectPolicy = projectServerPolicy(ctx.cwd, os.homedir(), isProjectApprovedSilently(ctx))
|
|
439
|
-
const { consented, gated } = splitByPolicy(applyServerPolicy(loadConfigFrom(projectConfigPaths(ctx.cwd)), policy), projectPolicy)
|
|
451
|
+
const { consented: consentedRaw, gated } = splitByPolicy(applyServerPolicy(loadConfigFrom(projectConfigPaths(ctx.cwd)), policy), projectPolicy)
|
|
452
|
+
// Claude's scope precedence is local over project: a name the local scope defines
|
|
453
|
+
// stays with the local (user-side) definition, so the project's entry is dropped
|
|
454
|
+
// here rather than allowed to shadow it.
|
|
455
|
+
const localNames = localScopeServerNames(os.homedir(), ctx.cwd)
|
|
456
|
+
const consented = Object.fromEntries(Object.entries(consentedRaw).filter(([name]) => !localNames.has(name)))
|
|
440
457
|
const projectWinners = new Set(Object.keys(consented))
|
|
441
458
|
const userServers = Object.fromEntries(Object.entries(scoped).filter(([name]) => !clients.has(name) && !projectWinners.has(name)))
|
|
442
459
|
// The consented project servers carry no ordering dependency on the user scope:
|
|
@@ -5,19 +5,19 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { DEFAULT_MAX_BYTES } from '@earendil-works/pi-coding-agent'
|
|
8
|
-
import { splitArgs } from '../internal/command-file.js'
|
|
9
8
|
import { capForContext } from '../internal/output-guard.js'
|
|
10
9
|
|
|
11
10
|
export function formatToolName(server: string, tool: string): string {
|
|
12
11
|
return `${server}_${tool}`.replaceAll('-', '_')
|
|
13
12
|
}
|
|
14
13
|
|
|
15
|
-
/** Claude exposes server prompts as /mcp__<server>__<prompt> slash commands
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
/** Claude exposes server prompts as /mcp__<server>__<prompt> slash commands: any
|
|
15
|
+
* character in the server name outside A-Za-z0-9_- becomes an underscore (hyphens
|
|
16
|
+
* stay), and the prompt name is used as the server declares it. Divergence: pi
|
|
17
|
+
* dispatches commands on the first whitespace-delimited token, so whitespace in the
|
|
18
|
+
* prompt name also folds to an underscore or the command would be unreachable. */
|
|
18
19
|
export function formatPromptCommandName(server: string, prompt: string): string {
|
|
19
|
-
|
|
20
|
-
return `mcp__${normalize(server)}__${normalize(prompt)}`
|
|
20
|
+
return `mcp__${server.replace(/[^A-Za-z0-9_-]/g, '_')}__${prompt.replace(/\s/g, '_')}`
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export interface McpPromptArgumentInfo {
|
|
@@ -32,17 +32,16 @@ export interface McpPromptInfo {
|
|
|
32
32
|
arguments?: McpPromptArgumentInfo[]
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
/** Claude passes prompt arguments space-separated after the command
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* no token are omitted, and the server enforces its own `required`. */
|
|
35
|
+
/** Claude passes prompt arguments space-separated after the command and "splits the
|
|
36
|
+
* arguments on whitespace, so each argument is a single token": no quote handling,
|
|
37
|
+
* one token per declared argument, extra trailing tokens dropped. Declared arguments
|
|
38
|
+
* with no token are omitted, and the server enforces its own `required`. */
|
|
40
39
|
export function mapPromptArguments(declared: ReadonlyArray<{ name: string }> | undefined, args: string): Record<string, string> {
|
|
41
|
-
const tokens =
|
|
40
|
+
const tokens = args.split(/\s+/).filter((token) => token !== '')
|
|
42
41
|
const names = (declared ?? []).map((argument) => argument.name)
|
|
43
42
|
const mapped: Record<string, string> = {}
|
|
44
43
|
for (let index = 0; index < names.length && index < tokens.length; index++) {
|
|
45
|
-
mapped[names[index]] =
|
|
44
|
+
mapped[names[index]] = tokens[index]
|
|
46
45
|
}
|
|
47
46
|
return mapped
|
|
48
47
|
}
|
|
@@ -17,38 +17,61 @@ import { FileOAuthProvider } from '../internal/mcp-oauth.js'
|
|
|
17
17
|
import { expandCwd, interpolateEnv, type ServerConfig, type StdioServerConfig } from './config.js'
|
|
18
18
|
import { runInteractiveOAuth, serializeInteractiveOAuth } from './oauth-flow.js'
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
// Claude's MCP_TIMEOUT default: 30 seconds per connect attempt.
|
|
21
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 30_000
|
|
21
22
|
// Claude's MCP_TOOL_TIMEOUT default is effectively hours: the per-call wall-clock budget
|
|
22
23
|
// is only a ceiling, and the idle timeout below is the real guard. 4h matches that model,
|
|
23
24
|
// so a legitimately slow-but-progressing tool is not killed at the old 2 minutes.
|
|
24
25
|
const DEFAULT_CALL_TIMEOUT_MS = 14_400_000
|
|
25
26
|
// The idle timeout: the longest a call may go with no response or progress before it is
|
|
26
|
-
// abandoned. Claude uses a separate idle guard
|
|
27
|
-
//
|
|
27
|
+
// abandoned. Claude uses a separate idle guard rather than the hours-long wall-clock
|
|
28
|
+
// budget, defaulting to five minutes for remote transports and 30 minutes for stdio
|
|
29
|
+
// servers; the SDK resets this window on every progress notification.
|
|
28
30
|
const DEFAULT_CALL_IDLE_TIMEOUT_MS = 300_000
|
|
31
|
+
const DEFAULT_STDIO_CALL_IDLE_TIMEOUT_MS = 1_800_000
|
|
32
|
+
|
|
33
|
+
/** Claude's numeric env vars accept scientific notation and digit-separator spellings
|
|
34
|
+
* (2e3 as 2000, 64_000 as 64000). A non-numeric value is undefined, not zero. */
|
|
35
|
+
function parseNumericEnv(raw: string): number | undefined {
|
|
36
|
+
const cleaned = raw.replaceAll('_', '')
|
|
37
|
+
if (cleaned.trim() === '') return undefined
|
|
38
|
+
const value = Number(cleaned)
|
|
39
|
+
return Number.isFinite(value) ? Math.floor(value) : undefined
|
|
40
|
+
}
|
|
29
41
|
|
|
30
42
|
/** A positive-integer env override, or the default when unset or unparseable. */
|
|
31
43
|
function envTimeout(name: string, fallback: number): number {
|
|
32
44
|
const raw = process.env[name]
|
|
33
45
|
if (raw === undefined) return fallback
|
|
34
|
-
const value =
|
|
35
|
-
return
|
|
46
|
+
const value = parseNumericEnv(raw)
|
|
47
|
+
return value !== undefined && value > 0 ? value : fallback
|
|
36
48
|
}
|
|
37
49
|
|
|
38
50
|
// Claude honors MCP_TIMEOUT (connect) and MCP_TOOL_TIMEOUT (per-call), both in ms.
|
|
39
51
|
export const connectTimeoutMs = (): number => envTimeout('MCP_TIMEOUT', DEFAULT_CONNECT_TIMEOUT_MS)
|
|
40
52
|
export const callTimeoutMs = (): number => envTimeout('MCP_TOOL_TIMEOUT', DEFAULT_CALL_TIMEOUT_MS)
|
|
41
53
|
|
|
54
|
+
/** Per-server inputs to the idle-window choice: the transport kind picks the default
|
|
55
|
+
* tier, and a per-server `timeout` of at least 1000 also floors the idle window. */
|
|
56
|
+
export interface ServerCallTuning {
|
|
57
|
+
stdio?: boolean
|
|
58
|
+
serverTimeoutMs?: number
|
|
59
|
+
}
|
|
60
|
+
|
|
42
61
|
/** The idle timeout in ms: the longest a call may go with no response or progress before
|
|
43
|
-
* it is abandoned
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
|
|
62
|
+
* it is abandoned. Defaults to Claude's tiers (five minutes remote, 30 minutes stdio),
|
|
63
|
+
* overridable by CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT, with 0 disabling it (leaving only
|
|
64
|
+
* the wall-clock budget). Unlike envTimeout, an explicit 0 is honored as "disabled"
|
|
65
|
+
* rather than falling back to the default. A per-server timeout of at least 1000 floors
|
|
66
|
+
* the enabled window, so a server granted a long wall budget is not idled out earlier. */
|
|
67
|
+
function idleTimeoutMs(tuning: ServerCallTuning): number {
|
|
47
68
|
const raw = process.env.CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
69
|
+
const override = raw === undefined ? undefined : parseNumericEnv(raw)
|
|
70
|
+
if (override === 0) return 0
|
|
71
|
+
const tierDefault = tuning.stdio ? DEFAULT_STDIO_CALL_IDLE_TIMEOUT_MS : DEFAULT_CALL_IDLE_TIMEOUT_MS
|
|
72
|
+
const base = override !== undefined && override > 0 ? override : tierDefault
|
|
73
|
+
const floor = tuning.serverTimeoutMs !== undefined && tuning.serverTimeoutMs >= 1000 ? tuning.serverTimeoutMs : 0
|
|
74
|
+
return Math.max(base, floor)
|
|
52
75
|
}
|
|
53
76
|
|
|
54
77
|
/** The SDK RequestOptions for a call under pi's two-tier timeout: a wall-clock ceiling and,
|
|
@@ -60,12 +83,20 @@ function idleTimeoutMs(): number {
|
|
|
60
83
|
* wall budget, only the wall budget applies. The outer withTimeout race is a wall-clock
|
|
61
84
|
* backstop and must be raced against `wall`, never the idle window, so a legitimately
|
|
62
85
|
* progressing call is not cut off. */
|
|
63
|
-
export function callRequestOptions(wall: number): { timeout: number; resetTimeoutOnProgress?: boolean; maxTotalTimeout?: number; onprogress?: () => void } {
|
|
64
|
-
const idle = idleTimeoutMs()
|
|
86
|
+
export function callRequestOptions(wall: number, tuning: ServerCallTuning = {}): { timeout: number; resetTimeoutOnProgress?: boolean; maxTotalTimeout?: number; onprogress?: () => void } {
|
|
87
|
+
const idle = idleTimeoutMs(tuning)
|
|
65
88
|
if (idle === 0 || idle >= wall) return { timeout: wall }
|
|
66
89
|
return { timeout: idle, resetTimeoutOnProgress: true, maxTotalTimeout: wall, onprogress: () => {} }
|
|
67
90
|
}
|
|
68
91
|
|
|
92
|
+
/** The tuning one server's config yields: its transport kind, and its declared
|
|
93
|
+
* per-server timeout. Per Claude, timeout values below 1000 are ignored and fall
|
|
94
|
+
* through to MCP_TOOL_TIMEOUT. */
|
|
95
|
+
export function serverCallTuning(config: ServerConfig): ServerCallTuning {
|
|
96
|
+
const declared = typeof config.timeout === 'number' && config.timeout >= 1000 ? config.timeout : undefined
|
|
97
|
+
return { stdio: isStdio(config), ...(declared !== undefined ? { serverTimeoutMs: declared } : {}) }
|
|
98
|
+
}
|
|
99
|
+
|
|
69
100
|
/** Claude reports a config entry that has a url but no type as an error; pi-code
|
|
70
101
|
* still connects (streamable HTTP with SSE fallback) but says the entry is wrong. */
|
|
71
102
|
/** An inline bearerToken (interpolated) wins over bearerTokenEnv, which names an
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-code",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.26",
|
|
4
4
|
"description": "Claude Code experience for the pi coding agent: reads your .claude config (rules, commands, skills, hooks, output styles, MCP servers, agents) and adds todo, checkpoints, memory, web, and subagents",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|