Summary
PULSE/modules/telegram.ts builds its query() options with settingSources: ["user", "project"] but never passes mcpServers. The Agent SDK does not infer MCP servers from setting sources, so the Telegram DA runs with no MCP servers at all — no Calendar, no Drive, no Gmail, no Keep, no custom servers — even when the same servers work perfectly in the terminal.
PULSE/modules/imessage.ts has the identical gap.
Why it happens
Two different config files, and only one of them is a "setting source":
claude mcp add writes to ~/.claude.json, top-level mcpServers key.
settingSources loads settings.json files only (user / project / local).
The SDK's own type declaration makes this explicit — mcpServers?: Record<string, McpServerConfig> is a separate option from settingSources, and its docstring for settingSources lists only the three settings.json locations.
So the servers are configured, the terminal sees them, and the SDK subprocess silently gets none.
Related prior issues show the same confusion from the other direction: #901 ("PAI repeatedly writes MCP config to settings.json instead of ~/.claude.json") and #646 ("MCP server configs in settings.json are silently ignored"). Both are about where MCP config lives; neither covers the SDK subprocess not receiving it.
Affected code
Checked against tag v7.1.1 and current main (4e33cc8). telegram.ts is byte-identical between the two — 1412 lines, mcpServers appears 0 times.
LifeOS/install/LIFEOS/PULSE/modules/telegram.ts:981 — settingSources present, mcpServers absent from the whole file.
LifeOS/install/LIFEOS/PULSE/modules/imessage.ts:161 — same.
The gap predates v7: it survived the v7.0.0 rewrite unchanged.
Reproduce
- Configure any MCP server with
claude mcp add and confirm it works in a terminal session.
- Message the DA on Telegram asking it to use that server (e.g. "add an event to my calendar").
- The DA reports it cannot.
Why this is worse than a missing feature
The failure is silent and self-explaining. The model cannot see how its own subprocess was configured, so when it finds no calendar tool it does not say "I have no calendar tool on this surface" — it invents a plausible cause. In our case it told the user the Calendar integration "was left out of the v6 port." That was false; the integration had been working in the terminal the entire time. The user believed it and dropped the request.
A missing tool is a fact the model can observe. The reason for the absence is not — and it will fill that gap with a confident guess.
Suggested fix
Read the configured servers and pass them explicitly. An allowlist is worth having: a phone-side agent should not automatically inherit every server the terminal has, especially metered ones.
// lib/mcp-servers.ts
export function loadMcpServers(
allow: readonly string[],
configPath = join(homedir(), ".claude.json"),
): Record<string, McpServerConfig> {
let configured: Record<string, McpServerConfig>
try {
configured = JSON.parse(readFileSync(configPath, "utf-8"))?.mcpServers ?? {}
} catch {
return {} // fail soft: degrade to "no MCP", never crash the polling loop
}
const out: Record<string, McpServerConfig> = {}
for (const name of allow) {
const server = configured[name]
if (server && typeof server === "object") out[name] = server
}
return out
}
// modules/telegram.ts, inside sdkOptions
settingSources: ["user", "project"],
mcpServers: loadMcpServers(TELEGRAM_MCP_SERVERS),
Two things worth pairing with it:
-
Block outbound sends at the canUseTool boundary once MCP is live. A bot surface that can send mail or chat messages on the principal's behalf is a much larger blast radius than a terminal session being watched. Drafting is a fine substitute — a draft waiting in Gmail is the right handoff. Calendar writes, deletion included, are recoverable and reasonable to allow.
-
Add an anti-confabulation rule to the Telegram system prompt. Something like: "If you lack a tool, say you lack the tool — never explain why you lack it. You cannot see how this surface was configured, so any reason you give is invented." This is the part that turns a config gap into misinformation, and it is worth fixing independently of the MCP wiring.
Note on which servers actually work headless
Worth documenting wherever MCP setup is described: connector-style MCP servers that authenticate interactively through claude.ai are absent from headless runs and cannot be relied on from a bot surface. A locally-run stdio server holding its own OAuth token does work. Both appear in a terminal session, so it is easy to wire up the wrong one.
Verified
With mcpServers passed, an SDK subprocess configured identically to the Telegram path enumerates the full calendar toolset, and the live Telegram bot now answers calendar questions correctly — confirmed end to end by the user from his phone.
For the negative side I only have indirect evidence: before the change the bot consistently reported having no calendar tool. I did not get a clean controlled run of the same subprocess with mcpServers omitted (the probe hit its turn limit), so treat "zero servers without the option" as read off the SDK's type contract and the observed behaviour, not as a measured result.
Environment
- Linux
@anthropic-ai/claude-agent-sdk — the repo's pinned ^0.1.0 range; reproduced on a 0.1.x resolution
- Reproduced on an older install whose
telegram.ts has local additions, but the construct at issue (sdkOptions built with settingSources and no mcpServers) is unchanged from the shipped file, and I verified the current release and main carry it verbatim.
Summary
PULSE/modules/telegram.tsbuilds itsquery()options withsettingSources: ["user", "project"]but never passesmcpServers. The Agent SDK does not infer MCP servers from setting sources, so the Telegram DA runs with no MCP servers at all — no Calendar, no Drive, no Gmail, no Keep, no custom servers — even when the same servers work perfectly in the terminal.PULSE/modules/imessage.tshas the identical gap.Why it happens
Two different config files, and only one of them is a "setting source":
claude mcp addwrites to~/.claude.json, top-levelmcpServerskey.settingSourcesloadssettings.jsonfiles only (user / project / local).The SDK's own type declaration makes this explicit —
mcpServers?: Record<string, McpServerConfig>is a separate option fromsettingSources, and its docstring forsettingSourceslists only the threesettings.jsonlocations.So the servers are configured, the terminal sees them, and the SDK subprocess silently gets none.
Related prior issues show the same confusion from the other direction: #901 ("PAI repeatedly writes MCP config to settings.json instead of ~/.claude.json") and #646 ("MCP server configs in settings.json are silently ignored"). Both are about where MCP config lives; neither covers the SDK subprocess not receiving it.
Affected code
Checked against tag
v7.1.1and currentmain(4e33cc8).telegram.tsis byte-identical between the two — 1412 lines,mcpServersappears 0 times.LifeOS/install/LIFEOS/PULSE/modules/telegram.ts:981—settingSourcespresent,mcpServersabsent from the whole file.LifeOS/install/LIFEOS/PULSE/modules/imessage.ts:161— same.The gap predates v7: it survived the v7.0.0 rewrite unchanged.
Reproduce
claude mcp addand confirm it works in a terminal session.Why this is worse than a missing feature
The failure is silent and self-explaining. The model cannot see how its own subprocess was configured, so when it finds no calendar tool it does not say "I have no calendar tool on this surface" — it invents a plausible cause. In our case it told the user the Calendar integration "was left out of the v6 port." That was false; the integration had been working in the terminal the entire time. The user believed it and dropped the request.
A missing tool is a fact the model can observe. The reason for the absence is not — and it will fill that gap with a confident guess.
Suggested fix
Read the configured servers and pass them explicitly. An allowlist is worth having: a phone-side agent should not automatically inherit every server the terminal has, especially metered ones.
Two things worth pairing with it:
Block outbound sends at the
canUseToolboundary once MCP is live. A bot surface that can send mail or chat messages on the principal's behalf is a much larger blast radius than a terminal session being watched. Drafting is a fine substitute — a draft waiting in Gmail is the right handoff. Calendar writes, deletion included, are recoverable and reasonable to allow.Add an anti-confabulation rule to the Telegram system prompt. Something like: "If you lack a tool, say you lack the tool — never explain why you lack it. You cannot see how this surface was configured, so any reason you give is invented." This is the part that turns a config gap into misinformation, and it is worth fixing independently of the MCP wiring.
Note on which servers actually work headless
Worth documenting wherever MCP setup is described: connector-style MCP servers that authenticate interactively through claude.ai are absent from headless runs and cannot be relied on from a bot surface. A locally-run stdio server holding its own OAuth token does work. Both appear in a terminal session, so it is easy to wire up the wrong one.
Verified
With
mcpServerspassed, an SDK subprocess configured identically to the Telegram path enumerates the full calendar toolset, and the live Telegram bot now answers calendar questions correctly — confirmed end to end by the user from his phone.For the negative side I only have indirect evidence: before the change the bot consistently reported having no calendar tool. I did not get a clean controlled run of the same subprocess with
mcpServersomitted (the probe hit its turn limit), so treat "zero servers without the option" as read off the SDK's type contract and the observed behaviour, not as a measured result.Environment
@anthropic-ai/claude-agent-sdk— the repo's pinned^0.1.0range; reproduced on a 0.1.x resolutiontelegram.tshas local additions, but the construct at issue (sdkOptionsbuilt withsettingSourcesand nomcpServers) is unchanged from the shipped file, and I verified the current release andmaincarry it verbatim.