Agent Tool Extension Development
Business plugins can register protocol-neutral Tools through
plugin.AgentHost. The MCP adapter reads the Registry only; it does not
enumerate plugin slugs or call plugin-specific transport APIs.
The low-level contract exists, but productized governance is incomplete. A read Tool can integrate when it accurately reuses an existing scope/RBAC meaning. Custom scopes and third-party write Tool approval/credential UI remain Phase 5.
Public Host and Lifecycle
type AgentHost interface {
AgentToolRegistry() *agent.Registry
AgentExecutor() *agent.Executor
}
Type-assert the smallest required host in Activate, keep every returned
handle, and revoke it in Deactivate. Do not assert the whole concrete Engine
to access undeclared internals.
type Plugin struct {
agentHandles []*agent.Handle
}
func (p *Plugin) Activate(app plugin.App) {
host, ok := app.(plugin.AgentHost)
if !ok || host.AgentToolRegistry() == nil {
return
}
handle, err := host.AgentToolRegistry().Register("example-reports", agent.Tool{
Name: "example.reports.summary",
Title: "Get report summary",
Description: "Return a bounded summary without private row data.",
InputSchema: json.RawMessage(`{"type":"object","properties":{},"additionalProperties":false}`),
OutputSchema: json.RawMessage(`{"type":"object","required":["count"],"properties":{"count":{"type":"integer","minimum":0}},"additionalProperties":false}`),
Mutability: agent.MutabilityRead,
Risk: agent.RiskRead,
Permission: agent.PermissionRequirement{
Scope: "gopress:site:read", Resource: "dashboard", Action: "read",
},
Timeout: 5 * time.Second, MaxConcurrency: 4,
Handler: func(ctx context.Context, call agent.Invocation) (any, error) {
return map[string]any{"count": 0}, nil
},
})
if err == nil {
p.agentHandles = append(p.agentHandles, handle)
}
}
func (p *Plugin) Deactivate(app plugin.App) {
for _, handle := range p.agentHandles {
handle.Revoke()
}
p.agentHandles = nil
}
Reusing dashboard.read is correct only when the summary truly has the same
safe semantics. Plugin-owned data normally needs plugin-owned scopes and RBAC.
The stock credential UI cannot issue arbitrary third-party scopes yet; do not
borrow unrelated Core permissions or create a weaker unaudited route.
Handler and Permission Rules
- Accept standard
context.Contextandagent.Invocation, never Gin or MCP request types. - Use plugin-owned services with the supplied context and bounded queries.
- Derive subject/owner from the refreshed Principal and stored resource, not request fields.
- Paginate lists and return only reviewed fields.
- Return stable
agent.Errorvalues and keep internal causes server-side. - Respect cancellation and avoid untracked background work.
Resource-dependent authorization belongs in ResolvePermission: parse the ID,
load the stored owner/type, return PermissionRequirement, and do no mutation.
Missing and cross-type resources should use a consistent Not Found response.
Write Tool Requirements and Current Gate
Every write must be idempotent, have a dedicated scope/action/risk, use a
required idempotency key, apply optimistic versioning to updates, separate
publish/destructive transitions, request confirmation where appropriate,
validate ownership in a transaction, return ResultForResource, and trigger
domain hooks/cache invalidation independently of MCP.
However, the current gopress-mcp Safe Write UI allow-lists only the six Core
write tools and derives issuable write scopes from them. A third-party write
Tool therefore remains blocked by stock policy even when registered. This is
an intentional safety gate, not something an extension should bypass.
Schema and Lifecycle Guidance
- Use
additionalProperties: falseand bounded strings/arrays/numbers. - Prefer type plus ID over a guessable ID alone.
- Keep outputs smaller than database DTOs and free of secrets/paths.
- Use the plugin namespace, not the Core-reserved
gopress.*prefix. - Roll back earlier handles if multi-tool activation partially fails.
- Test duplicate names, stale handles, deactivation, and revision changes.
Tests must cover missing scope/RBAC, owned vs foreign resources, cross-type ID, schema limits and invalid output, concurrent idempotency, optimistic conflict, panic/timeout/cancel audit, and clean deactivation.
Phase 5 needs a scope registry, admin approval, dynamic credential scope discovery, write-policy providers, risk review, and compatibility tests before third-party Safe Write is productized.
