Skip to main content
Version: v5

MCP Tools and Resources

Added in: v5.1.0

This page documents what the MCP server actually exposes — which tools land on tools/list for which user, how their input schemas are built, and what shows up in resources/list for each profile. Configuration knobs that gate this surface are documented in MCP Configuration.

Operations profile — tool generation

Tools are generated by walking Harper's OPERATION_FUNCTION_MAP and filtering through the configured allow/deny lists. Each tool is named for its operation (describe_all, search_by_value, system_information, …) and dispatches through the same chooseOperation + processLocalTransaction path the REST /operation endpoint uses, so existing verifyPerms enforcement runs unchanged.

Default-allow list

The default mcp.operations.allow list is intentionally narrow and read-only:

  • describe_* — schema / database / table descriptions.
  • list_* — enumerations (users, roles, databases).
  • search_* — search operations.
  • get_job, get_status, get_analytics, get_metrics — explicit safe getters.
  • system_information — server-level information.
  • read_log, read_audit_log — log readers.

get_* is deliberately not a wildcard. That glob would otherwise pull in:

  • get_configuration — returns TLS, S3, and authentication secrets.
  • get_components, get_component_file, get_custom_function, get_custom_functions — return component source code, which can embed secrets.
  • get_backup — backup metadata / payload.
  • get_deployment, get_deployment_payload — deployment artifacts.

These are all gated by verifyPerms, but defaulting to "expose them to the LLM if a super_user invokes them" is the wrong posture for MCP — the LLM provider sees and may log every input/output. Operators who want any of them on the surface opt them in via mcp.operations.allow.

Tool annotations

Each generated tool carries MCP annotations the client can use to decide how to surface it:

  • readOnlyHint: true — operations matching the read-only set (describe_*, list_*, search_*, get_*, read_*, system_information, status). MCP hosts can render these as "safe to call without confirmation".
  • destructiveHint: true — operations Harper knows are destructive (drop_*, delete*, restart*, set_configuration, remove_node). MCP hosts SHOULD prompt before invoking.

Neither hint is an authorization check — verifyPerms runs at dispatch.

Per-user filtering

tools/list is filtered through canRoleInvokeOperation so each session sees only the operations its user can actually call:

  • super_user sees everything in the allow list.
  • A user with structure_user: true sees schema-structure operations (create_schema, drop_table, create_attribute, etc.) in addition to anything in permission.operations.
  • Other users see only operations listed in permission.operations.

The list is cached per session and recomputed when a notifications/tools/list_changed event would fire.

Application profile — tool generation

The application profile walks Harper's Resources registry. For each exported Resource whose registration does not set exportTypes.mcp = false, Harper emits one MCP tool per implemented REST verb:

Verb on Resource prototypeTool nameSchema source
get(target, request)get_<name>Primary key + optional get_attributes
search(target, request)search_<name>conditions, operator, get_attributes, limit, cursor
post(target, data)create_<name>All writable attributes; non-nullable non-PK fields required
put(target, data)update_<name> (put)PK + writable attributes
patch(target, data)patch_<name> (patch)PK + writable attributes
delete(target, request)delete_<name>Primary key

A Resource that implements both put and patch emits update_<name> (favoring put).

Tool-name sanitization

The Resource's path is sanitized into a valid tool name: / and . become _. If two Resources sanitize to the same name, Harper disambiguates by prefixing the database name; if a collision still occurs, a 6-character hash suffix is appended.

Input schema derivation

Input schemas come from Table.attributes:

  • Harper types map to JSON Schema primitive types (Int/Long/BigIntinteger, Floatnumber, String/IDstring, Booleanboolean, Date[string, number], Bytes/Blobstring with contentEncoding: base64).
  • Nested Object and Array attributes recurse into their properties / elements.
  • nullable: true adds "null" to the type union.
  • Auto-managed columns (assignCreatedTime, assignUpdatedTime, expiresAt) and computed columns are stripped from write schemas (create_*, update_*) — the server fills them in.
  • Per-attribute attribute_permissions narrow the schema per requesting user: attributes the user cannot read are stripped from get_* / search_* schemas; attributes the user cannot insert/update are stripped from create_* / update_* schemas.

The schema narrowing is a UX optimization, not a security boundary — runtime Table.allowUpdate / Table.allowCreate still enforces. The narrowing just avoids burning LLM tokens on fields the user couldn't write anyway.

Custom mcpTools opt-in

A component author can expose non-verb instance methods as MCP tools by declaring a static mcpTools array on the Resource class:

class Orders extends Tables.orders {
static mcpTools = [
{
name: 'reconcile_unsettled',
method: 'reconcileUnsettled',
description: 'Reconcile all orders flagged as unsettled and emit a summary',
inputSchema: {
type: 'object',
properties: { since: { type: 'string', description: 'ISO 8601 timestamp' } },
},
},
];

async reconcileUnsettled({ since }) {
/* ... */
}
}

The corresponding instance method runs through Harper's normal transactional() envelope, so per-record allow* predicates and audit logging behave the same way as regular verb dispatch. Authentication is "is the user logged in" only — finer-grained gating is the method's responsibility.

exportTypes gating

The MCP surface mirrors the public REST surface. A Resource is filtered out of MCP enumeration entirely when its registration sets exportTypes.mcp = false:

server.http(Resource, { name: 'internal-thing', exportTypes: { mcp: false } });

This is independent of the http exportType — the only switch that operators set to scope MCP visibility is mcp.

Resources surface

Both profiles serve resources/list, resources/read, and resources/templates/list. The resources are static (no resources/subscribe in v1).

harper:// URIs

URIProfileContent
harper://aboutbothServer version, profile name, protocol versions, capabilities.
harper://operationsoperationsUser-filtered list of allowed operation names.
harper://openapiapplicationThe OpenAPI 3.0.3 document for the application's REST surface.
harper://schema/{database}/{table}applicationPer-table attribute definitions, RBAC-filtered at read time.

The schema URIs honor each user's permission[db].tables[table] walk — a user with no read or describe perm on a table gets a "permission denied" response from resources/read.

https:// URIs

The application profile additionally exposes every exported Resource (that passes the exportTypes.mcp gate and the hasRestVerbs check) as an https://<host>:<port>/<path> URI. These resolve in-process via Resources.getMatch(path, 'mcp') — there is no outbound HTTP request. The body returned by resources/read is a small descriptor:

{
"uri": "https://node.example.com:9926/Product",
"path": "Product",
"database": "data",
"table": "product",
"hint": "Use the corresponding `get_*` or `search_*` tool from `tools/list` to fetch records."
}

Per-record reads go through the tools surface, where each Resource's allow{Read,…} predicates run. The resources/read descriptor itself is a fast, side-effect-free hint — not a capability.

notifications/*/list_changed

After the initialize handshake, an MCP client opens GET /mcp to keep an SSE channel open for server-push frames. Harper subscribes to its existing role-cache and schema-reload event channels and, whenever one fires:

  1. Walks the per-worker session registry.
  2. For each session on that profile, re-resolves the bound user (so any role/permission mutations occurring between the handshake and the event are evaluated against current permissions, rather than a frozen snapshot).
  3. Recomputes the session's tools/list and resources/list against the fresh user.
  4. Compares to the snapshot taken at session start (or after the last fire).
  5. Emits notifications/tools/list_changed and / or notifications/resources/list_changed if and only if the visible set actually changed.

Sessions whose visible surface is unchanged see nothing — there is no broadcast. The notification carries no diff payload; clients call tools/list and resources/list again to fetch the new state.

The GET-SSE channel itself closes on:

  • Explicit DELETE /mcp (when mcp.session.allowClientDelete is true).
  • The session being TTL-evicted from system.mcp_session after mcp.session.idleTimeoutSeconds.
  • The client dropping the underlying TCP connection (Harper's HTTP server propagates that to the iterator's return(), which the registry's on-close listener catches).
  • An idle-prune sweep (belt-and-braces against the cases above missing — see Configuration / mcp.session.idleTimeoutSeconds).