Skip to main content
Version: v5

MCP tool payload sourcing

Harper's MCP server publishes tools via the Model Context Protocol (rev 2025-06-18, Streamable HTTP transport). This page is a reference for what fields appear on each generated tool descriptor and where the data comes from. For authoring guidance, see the Writing quality MCP and OpenAPI descriptions how-to.

Tool descriptor fields

Per the MCP spec, every tool descriptor returned from tools/list carries:

FieldRequiredPurpose
nameyesMachine identifier used by tools/call
descriptionyesLLM-facing prose explaining what the tool does and when to pick it over siblings
inputSchemayesJSON Schema for the arguments the tool accepts
outputSchemanoJSON Schema for the data the tool returns (added in spec rev 2025-06-18)
annotationsnoHints for clients: readOnlyHint, destructiveHint, idempotentHint, openWorldHint, title

Harper populates each from a different source depending on the tool's profile and origin.

Profiles

Harper exposes two MCP profiles, each with its own port and registration loop:

  • Operations profile — registers one MCP tool per Harper operation that survives the mcp.operations.allow / deny filter. Walks OPERATION_FUNCTION_MAP. The default allow list is read-only and intentionally narrow: describe_*, list_*, search_*, plus an explicit list of safe getters.
  • Application profile — registers verb tools (get_*, search_*, create_*, update_*, patch_*, delete_*) for every exported Resource, plus any custom tools declared via static mcpTools.

Sourcing per profile

Operations profile

For operations tools, the descriptor fields are sourced from:

FieldSource
nameOperation name (from OPERATION_FUNCTION_MAP key)
descriptionHand-authored entry in the operations descriptions catalog; falls back to a generic template when an operator opts in a non-cataloged operation
inputSchemaHand-curated JSON Schema in the operations input-schemas catalog; falls back to { type: 'object', additionalProperties: true }
annotations.readOnlyHinttrue if the operation matches a read-only prefix (describe_, list_, search_, get_, read_) or is the literal system_information
annotations.destructiveHinttrue for operations in the curated destructive set (drop_*, delete_*, restart, set_configuration, etc.)
annotations.idempotentHintDefault empty; opt-in per operation after end-to-end verification that the second call produces the same observable outcome

Operations registered outside core (for example, cluster_status from harper-pro) don't have catalog entries; they fall back to the generic description template until the per-operation metadata registry lands.

Application profile — verb tools

For verb tools generated from exported Resources:

FieldSource
name${verb}_${sanitized-path} (e.g. get_Product, search_Customer)
descriptionComposed: [ResourceClass.description \n\n] ${verb sentence} ${runtime RBAC note}
inputSchemaDerived per verb from ResourceClass.attributes and the caller's attribute_permissions. Per-attribute description propagates to inputSchema.properties[*].description
outputSchemaDerived per verb from ResourceClass.attributes for get_* / create_* / update_* / patch_*. delete_* returns { deleted: true, <pk> }. search_* deliberately omits outputSchema
annotations.readOnlyHinttrue on get_* and search_*
annotations.destructiveHinttrue on delete_*
annotations.idempotentHinttrue on update_* (PUT semantics); other verbs default off

static description and static properties on the Resource class override the auto-derived values. static outputSchemas[verb] overrides per-verb output schemas. static mcp.annotations[verb] overrides annotations per verb. static hidden === true suppresses the entire Resource from MCP listing.

Application profile — custom mcpTools

For tools declared via static mcpTools:

FieldSource
namedef.name from the mcpTools entry
descriptiondef.description from the entry; falls back to a generic template if omitted (with a warn-once log at registration)
inputSchemadef.inputSchema from the entry; falls back to { type: 'object', additionalProperties: true } if omitted (with a warn-once log at registration)
outputSchemadef.outputSchema from the entry (optional; no fallback)
annotationsdef.annotations from the entry (optional pass-through)

Custom tools have no per-user listing filter beyond authentication — the Resource's instance method is responsible for whatever RBAC it needs to enforce.

Sample descriptors

get_Product (with docstrings)

Given a Product table with type-level and field-level docstrings:

{
"name": "get_Product",
"description": "Product catalog row — what shows up in the storefront listing, search, and inventory feeds. One row per SKU.\n\nFetches a single Product record by sku. Runtime RBAC (allowGet) enforces per-record access at call time.",
"inputSchema": {
"type": "object",
"properties": {
"id": { "type": "string", "description": "Primary key (sku)." },
"get_attributes": { "type": "array", "items": { "type": "string" } }
},
"required": ["id"]
},
"outputSchema": {
"type": "object",
"properties": {
"sku": { "type": "string", "description": "Stock keeping unit — globally unique across catalogs." },
"name": { "type": "string", "description": "Display name shown in the storefront." },
"priceCents": { "type": "integer", "description": "Retail price in cents (USD)." }
},
"required": ["sku", "name", "priceCents"],
"additionalProperties": false
},
"annotations": { "readOnlyHint": true }
}

add_user (operations profile)

{
"name": "add_user",
"description": "Creates a new Harper user with username, password, and role. Requires super_user. Username is immutable after creation — use drop_user + add_user to rename.",
"inputSchema": {
"type": "object",
"properties": {
"username": { "type": "string" },
"password": { "type": "string" },
"role": { "type": "string", "description": "Role name. Use list_roles to discover available roles." },
"active": { "type": "boolean", "default": true }
},
"required": ["username", "password", "role"]
}
}

Note that add_user does NOT carry idempotentHint: true. Under MCP semantics this would claim the second call produces the same observable outcome — but add_user("bob") on first call returns the created user and on second call returns an "already exists" error. Different observable outcomes → not idempotent.

harper://* resources

Harper also publishes a small set of synthetic resources via the MCP resources/list endpoint:

URIProfileDescription
harper://aboutbothServer version, profile, MCP protocol versions
harper://operationsoperationsUser-filtered operations catalog
harper://openapiapplicationFull OpenAPI 3.0.3 document
harper://schema/{db}/{table}applicationPer-table schema, filtered by attribute_permissions
https://{host}/{path}applicationApplication HTTP Resources, in-process

For harper://schema/{db}/{table} and https://{host}/{path} entries, the descriptor description prepends Table.description / ResourceClass.description when present.

See also