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

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.

Returns:

  • (Boolean)


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_schemaObject

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