[Feature]: Gate the Studio guidance block on MCP state — it is injected even when every ekko-studio server is disabled
Summary
Every chat run that goes through Studio gets a guidance block appended to its instructions (getSystemPrompt()): the Ekko Studio MCP usage rules, the output-format spec, the mobile-device permission guards and the task-card rules. The block is appended regardless of whether the corresponding MCP servers are enabled.
After disabling all ekko-studio-* MCP servers, the text is still injected and still instructs the agent to call ekko_studio_api_openapi_get, ekko_studio_api_request, ekko_studio_browser_toolset, ekko_studio_use_toolset and ekko_studio_update_plan — none of which are in the tool payload anymore.
Measurements
Environment: hermes-web-ui 0.7.22, Hermes Agent runtime v0.21.3. Fresh session driven through Studio (source=cli).
- Persisted system prompt: 31,427 chars (~8,823 tokens).
- First API call of the session:
in=18,800prompt tokens (system prompt + tool schemas + first user message); later calls hit the cache at 96–99 %. - Tool payload with the
ekko-studio-*servers disabled: the 25 visible tools contain zeroekko_studio_*entries, and the deferred-tool catalog embedded in thetool_searchdescription contains none either.
Studio-injected text inside that system prompt:
| Block | Tokens | Source |
|---|---|---|
Ekko Studio MCP usage guidelines (6 bullets) |
336 | prompt.ts → HERMES_MCP_USAGE_GUIDELINES |
| Mobile-device permission guards (location / calendar + reminders / health) | 384 | sockets/chat-run.ts |
| Studio task-card instructions | 179 | services/task-plan-runs.ts |
| Output-format spec | 722 | prompt.ts → AI_OUTPUT_FORMAT_GUIDELINES |
X-Hermes-Profile header line |
34 | chat-run handler |
| Total | 1,655 | ≈ 8.8 % of the fixed prefix |
Strictly Ekko-related: 899 tokens (336 + 384 + 179), ≈ 4.8 % of the fixed prefix.
How this was measured (no instance-specific data needed): the persisted system prompt (state.db → system_prompts) was inspected directly and split on the block markers; the tool payload was dumped with the runtime's own tool-definition builder. Both are reproducible on any instance.
Source observation
packages/server/src/modules/studio/public/runs/prompt.ts
export function getSystemPrompt(
customPrompt?: string,
options?: { source?: string | null; outputLanguage?: 'zh' | 'en' },
): string {
const parts: string[] = [];
if (customPrompt) parts.push(customPrompt);
if (options?.source === 'workflow') parts.push(WORKFLOW_NODE_SYSTEM_CONTEXT.trim());
parts.push(HERMES_MCP_USAGE_GUIDELINES.join('\n')); // unconditional
parts.push(options?.outputLanguage === 'en'
? AI_OUTPUT_FORMAT_GUIDELINES_EN
: AI_OUTPUT_FORMAT_GUIDELINES);
return parts.join('\n\n');
}The constant's doc comment explains the intent — keep the block free of runtime values such as profile names or bearer tokens so the composed prompt stays stable and cache-friendly. That goal is reasonable, but "are the Studio MCP servers enabled for this profile" is a per-run constant rather than a per-token one: gating on it would not break prefix caching within a run, whereas today the text simply survives the MCP toolset being switched off.
Two consequences:
- Dead instructions. The prompt tells the agent to route documentation questions through
ekko_studio_api_openapi_getand to plan throughekko_studio_update_plan. With those tools absent, the model either burns a turn looking for them (tool_search/tool_describe) or falls back to raw HTTP calls that the guidance explicitly forbids. - Fixed context cost on every turn for users who do not use the Studio MCP toolset. Prompt caching hides most of the monetary cost, but these blocks still occupy the context window as part of the fixed prefix (see #1977 for the tool-schema side of the same problem).
One more detail in the same family: the device permission guards are appended whenever session_id exists and source ∉ {workflow, group_chat} (sockets/chat-run.ts), so desktop and web sessions also receive mobile location / calendar / health instructions even when no mobile device is attached to the run.
Relationship to #2401 / #2416
#2401 (fixed by #2416) was about this same block being injected twice. The dedupe works in 0.7.22 — verified: exactly one copy of each block in the persisted prompt. This report is about the block being injected unconditionally, which is a separate issue, and it is still present on the current release.
Suggested fix (any one of these is enough)
- Gate
HERMES_MCP_USAGE_GUIDELINESon the profile's MCP state: include it when at least oneekko-studio-*server is enabled, resolved once per run so the cached prefix stays stable within the run. - Or expose a profile/session-level opt-out (for example
studioGuidance: false, or a checkbox next to the MCP panel) for users who run Studio without the Studio MCP toolset. - Inject the mobile-device guards only when a mobile device target is actually attached to the run, instead of whenever a session id exists.
- Cheap intermediate step: collapse the six MCP bullets into a short one-liner and keep the full text only for runs where the MCP toolset is present.
Happy to test a build with any of these changes and report back with before/after token counts.
Source: EKKOLearnAI/hermes-studio