Skip to content

feat(mcp): hosted instance read tools - #7727

Open
andypalmi wants to merge 10 commits into
mainfrom
feat/mcp-tools-instance-config-read
Open

feat(mcp): hosted instance read tools#7727
andypalmi wants to merge 10 commits into
mainfrom
feat/mcp-tools-instance-config-read

Conversation

@andypalmi

@andypalmi andypalmi commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 1 read-only MCP tools for the hosted instance resource, added to forge/ee/lib/mcp/tools/instances.js, plus the two tools that span hosted and remote instances, added to a new forge/ee/lib/mcp/tools/shared-instances-devices.js. This covers per-instance configuration surfaces, instance observability, and the team-scoped instance lists.

Hosted instance tools (instances.js):

  • platform_get_hosted_instance_config - one configuration reader taking a sections array (any subset of ha, protection, autoUpdateStack; defaults to all). Backed by GET /projects/:id/ha, GET /projects/:id/protectInstance, and GET /projects/:id/autoUpdateStack, returning each requested section independently as { statusCode, data }.
  • platform_get_hosted_instance_custom_hostname - GET /projects/:id/customHostname, with an includeStatus boolean that additionally fetches the live verification status from GET /projects/:id/customHostname/status.
  • platform_list_hosted_instance_files - GET /projects/:id/files/_/:path
  • platform_get_hosted_instance_resources - GET /projects/:id/resources
  • platform_list_team_dashboard_instances - GET /teams/:teamId/dashboard-instances

Shared hosted/remote tools (shared-instances-devices.js):

  • platform_list_instance_http_tokens - takes instanceType (hosted or remote); lists the HTTP bearer tokens for a hosted or remote instance.
  • platform_get_instance_history - takes instanceType (hosted or remote); returns the timeline for a hosted or remote instance.
  • platform_get_instance_audit_log - takes instanceType (hosted or remote); reads the audit log for a hosted or remote instance as JSON. scope and includeChildren apply to hosted instances only and are rejected for a remote instance.

Tools that work against both hosted and remote instances live in shared-instances-devices.js rather than in instances.js or devices.js, so the shared surface is explicit; the owner-specific tools keep the one-file-per-resource convention.

All read tools are annotated readOnlyHint: true, destructiveHint: false. Several backing routes are plan-gated per team (High Availability, custom hostnames, protected instance, static file storage, HTTP bearer tokens, instance history, instance resources); a disabled feature and a genuine not-found both surface as a 404. The /resources/stream WebSocket route is intentionally not exposed; only the point-in-time /resources snapshot is implemented.

Consolidation notes

  • Config sections: platform_get_hosted_instance_ha, platform_get_hosted_instance_protection, and platform_get_hosted_instance_auto_update_stack are merged into platform_get_hosted_instance_config, which takes a sections array (subset of ha / protection / autoUpdateStack, defaults to all) and returns each requested section independently. When every requested section fails (for example the instance does not exist) the tool reports that failure status rather than a misleading 200.
  • Hostname plus status: platform_get_hosted_instance_custom_hostname and its status variant are merged, with an includeStatus boolean that also fetches the live verification status.
  • Audit format: the separate CSV export is removed; platform_get_instance_audit_log returns audit entries as JSON.
  • Instance-type twins: platform_list_instance_http_tokens, platform_get_instance_history and platform_get_instance_audit_log take an instanceType (hosted or remote) and serve both owner kinds, and are placed in shared-instances-devices.js. platform_get_instance_audit_log supersedes the remote-only platform_get_remote_instance_audit_log from feat(mcp): remote instance (device) read tools #7726.
  • Team listing: platform_list_hosted_instances now takes exactly one of teamId (team-wide, paginated and sortable) or applicationId (a single application, unpaginated); passing both, or neither, is rejected. The team-only sort, dir, and orderByMostRecentFlows parameters are rejected when applicationId is set.

Permissions

No permissions changes. #8185 removed the hardcoded user:expert-mcp scope allow-list, so expert MCP tokens now inherit the user's permissions gated by team role and no allow-list entries are needed.

Closes #7705

Test plan

  • eslint clean on the changed files
  • mocha unit tests

@codecov

codecov Bot commented Jul 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 76.27%. Comparing base (4732870) to head (bb0964d).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7727      +/-   ##
==========================================
+ Coverage   76.20%   76.27%   +0.06%     
==========================================
  Files         443      444       +1     
  Lines       23728    23797      +69     
  Branches     6315     6329      +14     
==========================================
+ Hits        18081    18150      +69     
  Misses       5647     5647              
Flag Coverage Δ
backend 76.27% <100.00%> (+0.06%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@andypalmi
andypalmi force-pushed the feat/mcp-tools-instance-config-read branch from 04324b4 to 6af352f Compare July 4, 2026 13:33
@andypalmi andypalmi linked an issue Jul 6, 2026 that may be closed by this pull request
@andypalmi
andypalmi force-pushed the feat/mcp-tools-shared-schemas branch from 8bfb22e to 9742987 Compare July 6, 2026 13:13
@andypalmi
andypalmi force-pushed the feat/mcp-tools-instance-config-read branch from 6af352f to fdafd47 Compare July 6, 2026 16:38
@andypalmi
andypalmi force-pushed the feat/mcp-tools-instance-config-read branch from fdafd47 to df8ad04 Compare July 6, 2026 22:01
@andypalmi andypalmi changed the title feat(mcp): instance configuration read tools feat(mcp): hosted instance read tools Jul 7, 2026
@andypalmi
andypalmi marked this pull request as draft July 30, 2026 08:22
@andypalmi
andypalmi force-pushed the feat/mcp-tools-shared-schemas branch from 103f9ae to 5773bdb Compare August 3, 2026 13:19
Add forge/ee/lib/mcp/schemas.js, a shared module of composable zod
fragments the platform read tools import instead of redefining entity-id
and pagination/search/sort/audit-log query fields in each tool file.

- entity-id params: teamId, applicationId, hostedInstanceId (UUID),
  remoteInstanceId, snapshotId
- query fragments composed per route by spreading only the params the
  backing finder honors: cursorParam/limitParam (basePagination),
  pageParam, searchQuery, sortParams, auditLogFilters
- appendQuery serialises a tool's supported params onto the request URL

The module lives one level above tools/ so the tool loader does not
register it as a tool module.

Closes #7669
@andypalmi
andypalmi force-pushed the feat/mcp-tools-shared-schemas branch from eae081f to e04360f Compare August 3, 2026 14:13
@andypalmi
andypalmi force-pushed the feat/mcp-tools-instance-config-read branch from 283ad01 to 4206424 Compare August 3, 2026 15:26
@andypalmi
andypalmi marked this pull request as ready for review August 4, 2026 10:33
@andypalmi
andypalmi requested a review from cstns August 4, 2026 10:33
@andypalmi
andypalmi force-pushed the feat/mcp-tools-instance-config-read branch from 4206424 to 526dcd3 Compare August 4, 2026 10:55
@andypalmi
andypalmi force-pushed the feat/mcp-tools-instance-config-read branch from 526dcd3 to da1f501 Compare August 4, 2026 11:04
@andypalmi andypalmi self-assigned this Aug 4, 2026
Base automatically changed from feat/mcp-tools-shared-schemas to main August 5, 2026 08:49
andypalmi added 2 commits August 19, 2026 15:15
…ayload shape

Return each platform_get_instance_config section as { statusCode, data }
so array payloads such as the autoUpdateStack schedule survive instead of
being spread into numeric keys.

Reword platform_get_hosted_instance_resources to describe a time-series of
usage samples rather than a point-in-time snapshot, and drop the claim in
both that tool and platform_get_instance_history that the not-enabled case
is rewritten into a friendly message (the routes return a plain not-found).

Note that flowLastUpdatedAt sort and orderByMostRecentFlows only apply when
includeLiveStatus is set, and drop the inert device:history token scope
(the remote history route checks project:history).
The audit-log export route responds with text/csv, but the MCP response
formatter always parses a success body as JSON, so requesting format=csv
failed. A downloadable CSV file cannot be delivered over the tool transport
anyway, so remove the format argument and always return JSON entries.
Comment thread forge/ee/lib/mcp/tools/instances.js
Comment thread forge/ee/lib/mcp/tools/instances.js Outdated
Comment thread forge/ee/lib/mcp/tools/instances.js Outdated
Comment thread forge/ee/lib/mcp/tools/instances.js Outdated
Comment thread forge/ee/lib/mcp/tools/instances.js Outdated
Comment thread forge/ee/lib/mcp/tools/instances.js Outdated
Comment thread forge/ee/lib/mcp/tools/instances.js Outdated
Comment thread forge/ee/lib/mcp/tools/instances.js Outdated
Comment thread forge/ee/lib/mcp/tools/instances.js
Comment thread forge/ee/lib/mcp/tools/instances.js Outdated
},
handler: async (args, { inject }) => {
const base = args.instanceType === 'remote' ? 'devices' : 'projects'
const url = appendQuery(`/api/v1/${base}/${args.instanceId}/history`, args, basePaginationKeys)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these api endpoints are just syntactic sugar over the audit log for which we already have tools for

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right that instance history is built on top of the audit log (AuditLog.forTimelineHistory). The one difference is the shape: the /history route returns the curated TimelineList (a filtered, deploy-oriented timeline) rather than raw audit entries, so it is a little more than a straight alias. I'm happy either way here: keep it for that curated view, or drop it and rely on the audit-log tools. Which would you prefer?

andypalmi added 2 commits August 21, 2026 13:57
…ce-config-read

# Conflicts:
#	forge/routes/auth/permissions.js
Move the tools shared between hosted and remote instances (list HTTP
tokens, get instance history) into a new tools/shared-instances-devices.js
so the shared surface is explicit, keeping the instanceType-discriminated
shape.

Rename the hosted-only tools to the hosted_instance convention:
platform_get_hosted_instance_config, platform_get_hosted_instance_custom_hostname
and platform_list_hosted_instance_files.

platform_get_hosted_instance_config reports the section failure status
when every requested section failed, instead of always returning 200.

platform_list_hosted_instances now requires exactly one of teamId or
applicationId, rejecting both-set and neither-set, and rejects the
team-only sort, dir and orderByMostRecentFlows parameters when scoped to
an application.

Document the file-store result shape and how to build the path, document
the audit-log scope and includeChildren parameters, and inline the
single-use audit-log schema fragments.
Move the instance audit-log reader into shared-instances-devices.js as
platform_get_instance_audit_log, taking an instanceType (hosted or
remote) and hitting the projects or devices audit-log route. Both routes
accept the same pagination and audit-log query params.

scope and includeChildren apply only to hosted instances (a remote
instance has no child entities), so they are rejected when instanceType
is remote.
@andypalmi

Copy link
Copy Markdown
Contributor Author

Testing results of new/changed tools

@cstns for visibility. Re-run after the review changes (shared file, renames, guards, and the shared audit-log tool).

Every tool below was invoked against a running local platform. Two teams were used: one with 10 hosted instances and no devices, and one with 9 hosted instances and 10 remote instances (devices), one of them online. Payloads are summarised with ids, names, cursors and timestamps redacted; structural values (counts, statuses, enum echoes, error codes) are verbatim.

platform_list_hosted_instances

Lists hosted instances for exactly one scope: a whole team (teamId, paginated and sortable) or a single application (applicationId, unpaginated). Passing both, or neither, is rejected, as are the team-only sort parameters when scoped to an application.

Case Input params Verdict Returned payload (overview, redacted)
team-wide default {teamId, limit:2} PASS {count:10, meta:{page:1,...}, instances:[{id,name,url,application,projectType,stack,template}]}
application-scoped {applicationId} PASS {count:11, instances:[{id,name,url}]} (unpaginated, no specification)
neither id {} PASS 400 invalid_request "Provide teamId to list a whole team, or applicationId to list a single application."
both ids {teamId, applicationId} PASS 400 invalid_request "Provide either teamId to list a whole team or applicationId to list a single application, not both."
application + sort {applicationId, sort:"name"} PASS 400 invalid_request "sort can only be used when listing across a whole team..."
team sort name asc {teamId, sort:"name", dir:"asc", limit:3} PASS names returned in ascending order
team includeLiveStatus {teamId, includeLiveStatus:true, limit:3} PASS per-item state populated [running, running, running]

platform_get_hosted_instance_config

Returns the requested config sections, each independently as {statusCode, data}. Reports success when at least one section was returned; reports the failure status when every requested section failed.

Case Input params Verdict Returned payload (overview, redacted)
default (all sections) {hostedInstanceId} PASS {ha:{statusCode:404}, protection:{statusCode:200}, autoUpdateStack:{statusCode:200}} - envelope 200 (a section succeeded)
subset autoUpdateStack {hostedInstanceId, sections:["autoUpdateStack"]} PASS {autoUpdateStack:{statusCode:200, data:[{hour,day,restart}]}} - array shape preserved
nonexistent instance {hostedInstanceId:"does-not-exist"} PASS envelope 404 isError; all sections {statusCode:404} (no longer a misleading 200)

platform_get_hosted_instance_custom_hostname

Returns the configured custom hostname; with includeStatus, also the live CNAME verification status.

Case Input params Verdict Returned payload (overview, redacted)
no status {hostedInstanceId} PASS 404 isError (none set / plan-gated)
includeStatus {hostedInstanceId, includeStatus:true} PASS 404 isError, content carries both {hostname} and {status} sub-objects

platform_list_hosted_instance_files

Lists files and directories at a path in the instance file store.

Case Input params Verdict Returned payload (overview, redacted)
root {hostedInstanceId, path:""} PASS {count:6, files:[{name, type:"file"

platform_get_hosted_instance_resources

Returns stored CPU/memory usage as a time-series of samples.

Case Input params Verdict Returned payload (overview, redacted)
valid id {hostedInstanceId} PASS {count:2039, resources:[{ps, src, ts}]}

platform_list_team_dashboard_instances

Returns the team's hosted instances that have the Node-RED dashboard installed.

Case Input params Verdict Returned payload (overview, redacted)
team id {teamId} PASS {count:1, projects:[{id, name, url, settings, ...}]}

platform_list_instance_http_tokens (shared)

Lists the HTTP bearer tokens for a hosted or remote instance, selected with instanceType.

Case Input params Verdict Returned payload (overview, redacted)
hosted {instanceId, instanceType:"hosted"} PASS {count:1, tokens:[1 token]} via /projects route
remote {instanceId, instanceType:"remote"} PASS {tokens:[]} via /devices route

platform_get_instance_history (shared)

Returns a chronological timeline for a hosted or remote instance, cursor-paginated.

Case Input params Verdict Returned payload (overview, redacted)
hosted {instanceId, instanceType:"hosted", limit:2} PASS {timeline:[2], meta:{next_cursor}} via /projects route
remote {instanceId, instanceType:"remote", limit:2} PASS {timeline:[1]} via /devices route

platform_get_instance_audit_log (shared)

Reads the audit log for a hosted or remote instance as JSON, selected with instanceType. scope and includeChildren apply to hosted instances only and are rejected for a remote instance.

Case Input params Verdict Returned payload (overview, redacted)
hosted, scope + includeChildren {instanceId, instanceType:"hosted", limit:3, scope:"project", includeChildren:true} PASS {log:[3], associations:{...}} via /projects route
remote {instanceId, instanceType:"remote", limit:3} PASS {log:[3]} via /devices route
remote + scope {instanceId, instanceType:"remote", scope:"device"} PASS 400 invalid_request "scope can only be used with hosted instances..."

Findings

  • The single-scope contract on platform_list_hosted_instances holds: exactly one of teamId/applicationId is required, both-set and neither-set are rejected, and the team-only sort parameters are rejected when scoped to an application.
  • platform_get_hosted_instance_config reports per-section {statusCode, data}, preserves the autoUpdateStack array shape, and now surfaces a 404 when the instance does not exist instead of a misleading 200.
  • The three shared tools (platform_list_instance_http_tokens, platform_get_instance_history, platform_get_instance_audit_log) route correctly to /projects or /devices from instanceType, and the remote audit-log guard rejects the hosted-only scope/includeChildren parameters.

Conclusion

All 9 tools added or changed in this PR verified against their descriptions, including every guard. No open defects.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5.11-a Hosted instance read tools (phase 1)

2 participants