MCP Tools and Resources
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_usersees everything in the allow list.- A user with
structure_user: truesees schema-structure operations (create_schema,drop_table,create_attribute, etc.) in addition to anything inpermission.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 prototype | Tool name | Schema 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/BigInt→integer,Float→number,String/ID→string,Boolean→boolean,Date→[string, number],Bytes/Blob→stringwithcontentEncoding: base64). - Nested
ObjectandArrayattributes recurse into theirproperties/elements. nullable: trueadds"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_permissionsnarrow the schema per requesting user: attributes the user cannot read are stripped fromget_*/search_*schemas; attributes the user cannot insert/update are stripped fromcreate_*/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
| URI | Profile | Content |
|---|---|---|
harper://about | both | Server version, profile name, protocol versions, capabilities. |
harper://operations | operations | User-filtered list of allowed operation names. |
harper://openapi | application | The OpenAPI 3.0.3 document for the application's REST surface. |
harper://schema/{database}/{table} | application | Per-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:
- Walks the per-worker session registry.
- 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).
- Recomputes the session's
tools/listandresources/listagainst the fresh user. - Compares to the snapshot taken at session start (or after the last fire).
- Emits
notifications/tools/list_changedand / ornotifications/resources/list_changedif 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(whenmcp.session.allowClientDeleteistrue). - The session being TTL-evicted from
system.mcp_sessionaftermcp.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).