Module: Mcpeye::Intent
- Defined in:
- lib/mcpeye/intent.rb
Overview
The injected-intent contract.
mcpeye’s cheap capture trick: the SDK injects an optional ‘mcpeyeIntent` parameter into every tool’s input schema. The agent self-reports, in its own words, why it is calling the tool and any blocker the user hit — so we capture intent at near-zero cost, with NO per-call LLM. The LLM runs later, in the worker, only to cluster sessions into reports.
The description below is what the agent reads. It is deliberately specific about surfacing failures/blockers AND naming any capability the user wanted that no tool provides — those attempted-but-failed asks, phrased as the user’s own unmet need, are the hero signal (the Intent Gap Report).
Keep INTENT_PARAM_DESCRIPTION byte-for-byte in sync with @mcpeye/core (packages/core/src/intent.ts) and the other SDKs (Python: packages/sdk-python/src/mcpeye/intent.py). Every server speaks the same contract; spec/intent_spec.rb asserts this string against the canonical text.
Constant Summary collapse
- INTENT_PARAM_NAME =
"mcpeyeIntent"- INTENT_PARAM_DESCRIPTION =
Byte-for-byte identical to packages/core/src/intent.ts INTENT_PARAM_DESCRIPTION. If you change one, change all SDKs (TS, Python, Ruby) together.
"Explain why you are calling this tool and how it fits into the user's overall workflow. " \ "This parameter is used only for product analytics and user-intent tracking. " \ "Write 25-35 words, in the third person. " \ "Exclude sensitive information such as credentials, passwords, or personal data. " \ "Describe any blocker or failure the user hit. " \ "Most important: if the user wanted to do something these tools cannot do, state the missing " \ "capability they needed, in their own words (for example: 'wanted to export the report as CSV, " \ "but no export tool exists')."
Class Method Summary collapse
-
.inject_intent_param(input_schema) ⇒ Object
Return a copy of a JSON-Schema object with the ‘mcpeyeIntent` property merged in.
-
.object_shaped?(schema) ⇒ Boolean
Whether a JSON-Schema fragment is object-shaped, i.e.
-
.param_json_schema ⇒ Object
JSON-Schema fragment merged into each tool’s inputSchema by the SDKs.
Class Method Details
.inject_intent_param(input_schema) ⇒ Object
Return a copy of a JSON-Schema object with the ‘mcpeyeIntent` property merged in. Non-destructive (the original schema and its properties are copied), mirroring the TS `augmentListToolsResult` / Python `inject_intent_param`:
-
Object-shaped schema (see ‘object_shaped?`): merge in `mcpeyeIntent`, defaulting `type` to “object”.
-
Anything else (explicit non-object type, or typeless-non-empty): returned unchanged — capture still works.
-
When ‘mcpeyeIntent` already exists (a tool owns the name), the property is left exactly as the tool declared it.
76 77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/mcpeye/intent.rb', line 76 def self.inject_intent_param(input_schema) return input_schema unless object_shaped?(input_schema) schema = input_schema.dup props = (schema["properties"] || schema[:properties] || {}).dup # Do not clobber a real tool param that happens to share the name. unless props.key?(INTENT_PARAM_NAME) || props.key?(INTENT_PARAM_NAME.to_sym) props[INTENT_PARAM_NAME] = param_json_schema end schema["properties"] = props schema["type"] ||= "object" schema end |
.object_shaped?(schema) ⇒ Boolean
Whether a JSON-Schema fragment is object-shaped, i.e. somewhere we can add an ‘mcpeyeIntent` string property. The single source of truth for object detection, shared by `inject_intent_param` and the Tracker’s in-place auto-injection so the two paths can never drift (matches Python’s guard):
-
‘type == “object”`, OR a `properties` key (string or symbol), OR a literally-empty `{}` (a parameterless tool — synthesize an object).
-
An explicit non-object type (e.g. ‘{ “type” => “array” }`) is NOT object shaped; a typeless-but-non-empty schema (e.g. `{ “description” => “x” }`) is NOT either — there is no sensible place to add the param, and capture still works without it.
57 58 59 60 61 62 63 64 |
# File 'lib/mcpeye/intent.rb', line 57 def self.object_shaped?(schema) return false unless schema.is_a?(Hash) explicit_type = schema["type"] || schema[:type] explicit_type == "object" || schema.key?("properties") || schema.key?(:properties) || (explicit_type.nil? && schema.empty?) end |
.param_json_schema ⇒ Object
JSON-Schema fragment merged into each tool’s inputSchema by the SDKs. Returns a fresh Hash each call so a caller mutating it can never corrupt the shared contract.
39 40 41 42 43 44 |
# File 'lib/mcpeye/intent.rb', line 39 def self.param_json_schema { "type" => "string", "description" => INTENT_PARAM_DESCRIPTION } end |