[Feature]: Gate the Studio guidance block on MCP state — it is injected even when every ekko-studio server is disabled

Author: liuweilhyCreated Sep 18, 2026Updated Sep 18, 2026

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,800 prompt 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 zero ekko_studio_* entries, and the deferred-tool catalog embedded in the tool_search description contains none either.

Studio-injected text inside that system prompt:

Block Tokens Source
Ekko Studio MCP usage guidelines (6 bullets) 336 prompt.tsHERMES_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.tsAI_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.dbsystem_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

typescript
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:

  1. Dead instructions. The prompt tells the agent to route documentation questions through ekko_studio_api_openapi_get and to plan through ekko_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.
  2. 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)

  1. Gate HERMES_MCP_USAGE_GUIDELINES on the profile's MCP state: include it when at least one ekko-studio-* server is enabled, resolved once per run so the cached prefix stays stable within the run.
  2. 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.
  3. Inject the mobile-device guards only when a mobile device target is actually attached to the run, instead of whenever a session id exists.
  4. 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