Skip to content

feat(tools): MCP sampling Phase 2 — spec coverage, governance & audit - #366

Closed
eren-karakus0 wants to merge 1 commit into
NousResearch:mainfrom
eren-karakus0:feat/mcp-sampling-phase2
Closed

feat(tools): MCP sampling Phase 2 — spec coverage, governance & audit#366
eren-karakus0 wants to merge 1 commit into
NousResearch:mainfrom
eren-karakus0:feat/mcp-sampling-phase2

Conversation

@eren-karakus0

Copy link
Copy Markdown
Contributor

Summary

Extends the MCP sampling/createMessage implementation with full MCP spec compliance, per-server governance controls, and structured audit observability. Built on top of Phase 1 (PR #64).

Spec Coverage

  • Bug fix: _map_stop_reason now correctly maps tool_calls"toolUse" (was incorrectly "endTurn")
  • tools/toolChoice forwarding: Server-provided tool definitions are forwarded to the LLM as OpenAI function-calling format, with toolChoice mode mapping (auto/required/none)
  • Tool content blocks: Full support for tool_use and tool_result content blocks in _convert_sampling_messages — converts to OpenAI tool_calls and role: "tool" messages respectively
  • Tool use responses: When LLM responds with tool_calls, returns CreateMessageResult with stopReason: "toolUse" and tool use content blocks
  • Malformed args safety: json.loads() on tool call arguments is now wrapped in try/except — malformed JSON falls back to raw string instead of crashing the callback
  • Mixed content lists: Content lists containing both text and tool_result blocks are now handled correctly (previously tool_results were silently dropped as "unsupported")

Governance

  • allowed_models: Per-server model whitelist. Empty list (default) permits any model. Non-listed models return structured ErrorData
  • max_tool_rounds: Caps consecutive tool use rounds per server (default: 5). Prevents infinite tool loops between LLM and server. Set to 0 to disable tool loops entirely. Counter resets on normal text response

Audit Observability

  • _sampling_metrics: Per-server counters tracking requests, errors, tokens_used, and tool_use_count
  • get_mcp_status(): Metrics exposed in server status for monitoring/debugging
  • log_level: Config-driven audit verbosity ("debug", "info", "warning")

Config Schema (all backward-compatible)

mcp_servers:
  my_server:
    sampling:
      # Existing (unchanged):
      enabled: true
      model: "gemini-3-flash"
      max_tokens_cap: 4096
      timeout: 30
      max_rpm: 10
      # New:
      allowed_models: []        # model whitelist (empty = all)
      max_tool_rounds: 5        # tool loop limit (0 = disable)
      log_level: "info"         # audit verbosity

All new fields have defaults — zero breaking changes for existing configurations.

Files Changed

File Change Delta
tools/mcp_tool.py Bug fix, tool content conversion, tools/toolChoice forwarding, governance, metrics +170 lines
tests/tools/test_mcp_tool.py 31 new tests across 7 test classes +560 lines
docs/mcp.md Config table, Tool Use section, Per-Server Policy section +90 lines
skills/mcp/native-mcp/SKILL.md Config table, tool use reference +15 lines

Test Plan

  • pytest tests/tools/test_mcp_tool.py -q151 passed, 3 pre-existing fail (mcp pkg not installed)
  • pytest tests/tools/test_mcp_tool.py -k Sampling -W error::RuntimeWarning → 0 warnings
  • All 31 new tests pass: stopReason fix, tool content conversion (6), tools/toolChoice forwarding (6), audit metrics (4), allowed_models (3), max_tool_rounds (4), log_level (1), malformed args (3), mixed content list (3), existing test updated (1)
  • Backward compatibility: existing 120 tests unaffected
  • Tested on Windows 11 with Python 3.13.5

Related

…d audit

Extend MCP sampling with full spec compliance, per-server governance
controls, and structured audit observability.

Spec coverage:
- Fix _map_stop_reason: tool_calls now correctly maps to "toolUse"
- Add tools/toolChoice forwarding to LLM (OpenAI function-calling format)
- Handle tool_use and tool_result content blocks in message conversion
- Return tool_use content blocks in CreateMessageResult for tool responses
- Graceful fallback for malformed JSON in tool_calls arguments
- Handle mixed content lists (text + tool_result) without silent data loss

Governance:
- allowed_models: per-server model whitelist (empty = allow all)
- max_tool_rounds: cap consecutive tool use rounds (default: 5, 0 = disable)
- Both return structured ErrorData on violation

Audit:
- _sampling_metrics: per-server request/error/token/tool_use counters
- Metrics exposed via get_mcp_status() for observability
- log_level config: control audit verbosity (debug/info/warning)

All new config fields have defaults — zero breaking changes for existing configs.

Tests: 31 new tests (151 total passed, 3 pre-existing fail from missing mcp pkg)
teknium1 added a commit that referenced this pull request Mar 9, 2026
Add MCP sampling/createMessage capability allowing MCP servers to request
LLM completions through the Hermes agent during tool execution. Enables
agent-in-the-loop workflows (data analysis, content generation, decision
making) where servers can leverage the LLM as needed.

Implementation as SamplingHandler class (per-server instance, no globals):
- Text-only sampling: server asks LLM a question, gets text back
- Tool use in sampling: server provides tools, LLM can use them in a
  multi-turn loop with configurable max_tool_rounds governance
- Rate limiting (sliding window, configurable max_rpm per server)
- Model resolution (config override > server hint > default)
- Model whitelist (allowed_models per server)
- Token cap (max_tokens_cap per server)
- LLM timeout with asyncio.wait_for
- Credential stripping on responses
- Per-server audit metrics (requests, errors, tokens_used, tool_use_count)
- Configurable log_level for audit verbosity
- Non-blocking: LLM calls offloaded via asyncio.to_thread()
- Proper MCP SDK types: CreateMessageResult for text responses,
  CreateMessageResultWithTools + ToolUseContent for tool use responses
- SamplingCapability with SamplingToolsCapability advertised to servers
- Backward compatible: silently disabled if MCP SDK lacks sampling types

Config (all optional, zero breaking changes):
  mcp_servers:
    my_server:
      sampling:
        enabled: true        # default
        model: 'gemini-3-flash'
        max_tokens_cap: 4096
        timeout: 30
        max_rpm: 10
        allowed_models: []
        max_tool_rounds: 5
        log_level: 'info'

Based on the sampling concept from PR #366 by eren-karakus0. Restructured
as a class-based design, fixed critical bugs (wrong return types for tool
use, missing capability advertisement, broken Pydantic validation), and
added tests using real MCP SDK types.

50 new tests, full suite passes (2600 tests).
@teknium1

teknium1 commented Mar 9, 2026

Copy link
Copy Markdown
Contributor

Thanks for this work! The sampling concept and the thorough documentation were valuable. We've incorporated the feature in PR #753, restructured as a class-based SamplingHandler design with some bug fixes (the tool use path needed CreateMessageResultWithTools + ToolUseContent instead of CreateMessageResult with raw dicts, and SamplingCapability needed to advertise tool support). You're credited in the commit message.

Closing this PR since the branch was 388 commits behind main and couldn't be merged directly, but the feature is landing. Thanks again for the contribution!

@teknium1 teknium1 closed this Mar 9, 2026
teknium1 added a commit that referenced this pull request Mar 9, 2026
Add MCP sampling/createMessage capability via SamplingHandler class.

Text-only sampling + tool use in sampling with governance (rate limits,
model whitelist, token caps, tool loop limits). Per-server audit metrics.

Based on concept from PR #366 by eren-karakus0. Restructured as class-based
design with bug fixes and tests using real MCP SDK types.

50 new tests, 2600 total passing.
@eren-karakus0

Copy link
Copy Markdown
Contributor Author

Glad to see MCP sampling landed in #753! Looks like it covers similar ground to what I proposed here. Happy to have contributed to the direction.

m0at pushed a commit to m0at/hermes-agent that referenced this pull request Mar 16, 2026
ManagedServer in this branch passes tools= to apply_chat_template(),
enabling proper tool calling for Phase 2 (RL training with logprobs).

@SafariShow SafariShow left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hermes Agent Review

Found 1 critical issue, 1 warning, and a few minor suggestions.

🔴 Critical

  • apps/dashboard/server/api/ai/personalize-template.post.ts and apps/dashboard/server/api/ai/template-events.post.ts: Missing authentication checks. The comment says "Auth: requires the same Bearer/cookie token", but neither endpoint actually verifies the user. An attacker can hit personalize-template to drain Anthropic API tokens via Claude generation, and hit template-events to spam the builder_events table with junk. You need to call getTemplateAccessContext or similar auth middleware before processing the body.

⚠️ Warnings

  • apps/dashboard/server/utils/templateEvents.ts (Lines 76-88): emitTemplateEvent triggers a db.insert() without returning a Promise, and it is not passed to event.waitUntil(). In serverless environments (like Vercel), if the HTTP response is returned before the background DB insert completes, the process will be suspended and the event will be dropped. Either pass the H3Event to use event.waitUntil(), or simply await it with a .catch() in the endpoint.
  • Request Body Validation: readBody(event) as RequestBody is used without Zod parsing. If body.products is passed as a string or object instead of an array, the application will crash at runtime when calling .map() or .length.

💡 Suggestions

  • apps/dashboard/server/ai/template-personalizers/listicle.ts: The file header comment says "Per-item copy rewrite is intentionally deferred to Phase 2", but the code actually does implement rewriteListicleItemsWithLLM! The docstring is stale and should be updated to match the Phase 3 implementation.
  • apps/dashboard/scripts/seed-global-templates.ts: Contains several console.log statements for progress reporting. Perfectly fine for a CLI script, but noting it here in case you intended to use a structured logger.

✅ Looks Good

  • The architectural split between the generic substitution (Floor) and the LLM rewrite (Ceiling) is very clean and provides a great fallback mechanism.
  • Great job adding the Authorization: Bearer fallback in resolveAuthToken to support SDK/MCP callers!
  • Zod schemas in llm-copy.ts for structured outputs are well-described and strict.

angelburgosrosado pushed a commit to angelburgosrosado/hermes-agent that referenced this pull request Apr 27, 2026
…Research#753)

Add MCP sampling/createMessage capability via SamplingHandler class.

Text-only sampling + tool use in sampling with governance (rate limits,
model whitelist, token caps, tool loop limits). Per-server audit metrics.

Based on concept from PR NousResearch#366 by eren-karakus0. Restructured as class-based
design with bug fixes and tests using real MCP SDK types.

50 new tests, 2600 total passing.
angelburgosrosado pushed a commit to angelburgosrosado/hermes-agent that referenced this pull request Apr 28, 2026
ManagedServer in this branch passes tools= to apply_chat_template(),
enabling proper tool calling for Phase 2 (RL training with logprobs).
angelburgosrosado pushed a commit to angelburgosrosado/hermes-agent that referenced this pull request Apr 28, 2026
Add MCP sampling/createMessage capability allowing MCP servers to request
LLM completions through the Hermes agent during tool execution. Enables
agent-in-the-loop workflows (data analysis, content generation, decision
making) where servers can leverage the LLM as needed.

Implementation as SamplingHandler class (per-server instance, no globals):
- Text-only sampling: server asks LLM a question, gets text back
- Tool use in sampling: server provides tools, LLM can use them in a
  multi-turn loop with configurable max_tool_rounds governance
- Rate limiting (sliding window, configurable max_rpm per server)
- Model resolution (config override > server hint > default)
- Model whitelist (allowed_models per server)
- Token cap (max_tokens_cap per server)
- LLM timeout with asyncio.wait_for
- Credential stripping on responses
- Per-server audit metrics (requests, errors, tokens_used, tool_use_count)
- Configurable log_level for audit verbosity
- Non-blocking: LLM calls offloaded via asyncio.to_thread()
- Proper MCP SDK types: CreateMessageResult for text responses,
  CreateMessageResultWithTools + ToolUseContent for tool use responses
- SamplingCapability with SamplingToolsCapability advertised to servers
- Backward compatible: silently disabled if MCP SDK lacks sampling types

Config (all optional, zero breaking changes):
  mcp_servers:
    my_server:
      sampling:
        enabled: true        # default
        model: 'gemini-3-flash'
        max_tokens_cap: 4096
        timeout: 30
        max_rpm: 10
        allowed_models: []
        max_tool_rounds: 5
        log_level: 'info'

Based on the sampling concept from PR NousResearch#366 by eren-karakus0. Restructured
as a class-based design, fixed critical bugs (wrong return types for tool
use, missing capability advertisement, broken Pydantic validation), and
added tests using real MCP SDK types.

50 new tests, full suite passes (2600 tests).
02356abc pushed a commit to 02356abc/hermes-agent that referenced this pull request May 14, 2026
…Research#753)

Add MCP sampling/createMessage capability via SamplingHandler class.

Text-only sampling + tool use in sampling with governance (rate limits,
model whitelist, token caps, tool loop limits). Per-server audit metrics.

Based on concept from PR NousResearch#366 by eren-karakus0. Restructured as class-based
design with bug fixes and tests using real MCP SDK types.

50 new tests, 2600 total passing.
gizdusum pushed a commit to gizdusum/hermes-agent that referenced this pull request May 17, 2026
ManagedServer in this branch passes tools= to apply_chat_template(),
enabling proper tool calling for Phase 2 (RL training with logprobs).
waefrebeorn pushed a commit to waefrebeorn/slermes that referenced this pull request Jul 2, 2026
…Research#753)

Add MCP sampling/createMessage capability via SamplingHandler class.

Text-only sampling + tool use in sampling with governance (rate limits,
model whitelist, token caps, tool loop limits). Per-server audit metrics.

Based on concept from PR NousResearch#366 by eren-karakus0. Restructured as class-based
design with bug fixes and tests using real MCP SDK types.

50 new tests, 2600 total passing.
nepenth pushed a commit to nepenth/hermes-agent that referenced this pull request Aug 7, 2026
Add MCP sampling/createMessage capability allowing MCP servers to request
LLM completions through the Hermes agent during tool execution. Enables
agent-in-the-loop workflows (data analysis, content generation, decision
making) where servers can leverage the LLM as needed.

Implementation as SamplingHandler class (per-server instance, no globals):
- Text-only sampling: server asks LLM a question, gets text back
- Tool use in sampling: server provides tools, LLM can use them in a
  multi-turn loop with configurable max_tool_rounds governance
- Rate limiting (sliding window, configurable max_rpm per server)
- Model resolution (config override > server hint > default)
- Model whitelist (allowed_models per server)
- Token cap (max_tokens_cap per server)
- LLM timeout with asyncio.wait_for
- Credential stripping on responses
- Per-server audit metrics (requests, errors, tokens_used, tool_use_count)
- Configurable log_level for audit verbosity
- Non-blocking: LLM calls offloaded via asyncio.to_thread()
- Proper MCP SDK types: CreateMessageResult for text responses,
  CreateMessageResultWithTools + ToolUseContent for tool use responses
- SamplingCapability with SamplingToolsCapability advertised to servers
- Backward compatible: silently disabled if MCP SDK lacks sampling types

Config (all optional, zero breaking changes):
  mcp_servers:
    my_server:
      sampling:
        enabled: true        # default
        model: 'gemini-3-flash'
        max_tokens_cap: 4096
        timeout: 30
        max_rpm: 10
        allowed_models: []
        max_tool_rounds: 5
        log_level: 'info'

Based on the sampling concept from PR NousResearch#366 by eren-karakus0. Restructured
as a class-based design, fixed critical bugs (wrong return types for tool
use, missing capability advertisement, broken Pydantic validation), and
added tests using real MCP SDK types.

50 new tests, full suite passes (2600 tests).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants