Extending agent access
Add a tool, a server profile, a provider, a skill, a path handler and a permission to Scipio's agent access layer.
This page shows how to extend framework/mcp: a tool, a service tool, a
server profile, a provider, a skill, a path handler, a permission, and how
to smoke-test a change with curl. Read
framework/resources/templates/mcp/README.txt first for the starter files.
1. Add a tool#
A tool is one static method on a class an @McpServer annotation already
covers. Add a public static method under your component’s src/.../mcp/
package; the first parameter is always McpCallContext ctx, each other
parameter carries an @McpParam with an explicit name. Annotate the
method with @McpTool: a topic, a name in snake_case, a short
description, readOnly = true when the tool only reads data. The topic is
the tool the agent sees; the name is the action it passes. Every method that
names the same topic joins the same tool, which is why one application shows an
agent five tools and not fifty. Return a Map, a List,
a GenericValue, a String, or an McpResult; convert a raw service
result with ResultConverter.toJsonMap(result). Throw McpToolException
for a tool error, or McpToolException.denied(msg) for a permission
denial.
@McpTool(topic = "order", name = "find", description = "Find orders by id, status, or party.",
readOnly = true, order = 10)
public static Object findOrders(McpCallContext ctx,
@McpParam(name = "orderId", required = false) String orderId,
@McpParam(name = "statusId", required = false) String statusId,
@McpParam(name = "limit", required = false) Integer limit) {
Map<String, Object> result = ctx.runService("findOrders", ctx.serviceContext(
Map.of("orderId", orderId, "statusId", statusId, "limit", ctx.limit(limit))));
return ResultConverter.toJsonMap(result);
}The agent calls the tool order with action set to find. Declare the topic
once on @McpServer(topics = ...) or @McpServerExtension(topics = ...) to
give it its own description and order.
A read-only tool needs only the webapp’s _VIEW permission and works with
a readOnly=Y token. Every other tool needs _UPDATE, unless permission
names a different one.
2. Add a server profile#
A server profile is one class annotated with @McpServer, usually one per
component. Copy framework/resources/templates/mcp/ExampleMcp.java into
your component; set name, component (the directory name, for example
order) and description. List readable entities in entities and
services ranked first in search in featuredServices. A service tool
wraps an existing service directly: serviceTools = { @McpServiceTool(service = "...", name = "...") }, for a service that
already does the whole job. No build file change is needed: the
scipio-component Gradle plugin adds framework:mcp to every
application, addon and hot-deploy component (scipioComponent { agentTools.set(false) } opts out). Set allowAnonymous = true only for a
public-facing server, such as the shop assistant, with its public tools
marked access = McpAccess.PUBLIC. Give a tool an order (lower first)
when list order matters; featured tools sort first at equal order, and a
profile’s own tools always precede the core tools.
2.1 Extend a server from another component#
An addon, a hot-deploy component or a customer project adds tools to a
server it does not own with @McpServerExtension, leaving the server’s
own source unchanged. Copy
framework/resources/templates/mcp/ExampleMcpExtension.java; set server
to the server name (for example order) and add featuredServices,
entities, serviceTools and providers as on @McpServer. Add
@McpTool, @McpResource and @McpPrompt methods as in section 1, with
your own name prefix; a tool name the server already defines is skipped
with a warning. Reload the registry (2.2) so the tools appear on the
server’s endpoint and in scipio_apps with action tools.
2.2 Disable a tool, reload the registry#
mcp.tool.disable in mcp.properties lists tool names
(scipio_entity), whole topics (shop.shop_cart) or one action of a
topic (shop.shop_cart:checkout)
removed from every tool list. Webtools > Agent Access > Skills > “Reload
agent registry” (or scipio_admin with action reload_registry,
MCP_ADMIN) drops the
server registry, the skills, the service catalog and the policy caches
and rebuilds them on the next MCP request; a new class still needs a
component reload or a restart, but a new or changed SKILL.md needs only
the reload.
3. Add a provider#
A provider covers a tool set a static annotation cannot express, such as a
list built at run time from data. Implement
com.ilscipio.scipio.mcp.registry.McpToolProvider, list the class in
@McpServer(providers = {MyProvider.class}), and follow the pattern in
the built-in providers, CoreToolProvider and SkillProvider, for the
exact method shape.
4. Add a skill#
A skill is one SKILL.md file that tells an agent how to use a server’s
tools. Copy framework/resources/templates/mcp/SKILL.md into
applications/<app>/skills/<name>/SKILL.md (or the matching path under
framework/, addons/, or hot-deploy/). Fill in the frontmatter:
name, one-sentence description, metadata.scipio-server (the server
name from section 2). Write the body in short, direct sentences, a
numbered list for a procedure, exact tool names, field names and status
ids, 60 to 120 lines total, every tool name in backticks: the registry
checks each backticked tool-like name against the real tools and reports
a wrong one as a warning in the log, in scipio_apps with action
skill_list and on
Webtools > Agent Access > Skills. Click “Reload agent registry” (or
restart); the skill then appears in scipio_apps with action
skill_list, as the resource
scipio://skills/<name>, and in ./gradlew assembleAgentPlugin’s zip.
5. Add a path handler#
A path handler serves one URL prefix in every webapp at once, ahead of the
normal controller path check. framework/mcp uses this for /mcp itself;
use it only for a new agent-facing path, not ordinary application screens.
public interface WebappPathHandler {
String pathPrefix();
boolean handle(HttpServletRequest req, HttpServletResponse res, WebappInfo info);
}Annotate the class with @WebappPathHandlerDef, set priority only when
two handlers may claim the same prefix (lower runs first), and give it a
public no-argument constructor; WebappPathHandlerRegistry discovers it
automatically, no registration file needed. Never call
request.getSession() inside a path handler: this keeps the path free of
Visit rows and any dev auto-login escalation.
6. Add a permission#
Follow the SecurityPermission seed pattern framework/mcp itself uses.
<SecurityPermission permissionId="EXAMPLE_AGENT_ACTION"
description="Let an agent run the example action."/>
<SecurityGroupPermission groupId="SCIPIO_AGENT" permissionId="EXAMPLE_AGENT_ACTION"/>
<SecurityGroupPermission groupId="FULLADMIN" permissionId="EXAMPLE_AGENT_ACTION"/>Grant SCIPIO_AGENT when every agent should have it by default, and
FULLADMIN so a human admin always has it too, then reference it from a
tool’s permission attribute (or leave the tool on the default webapp
base permission when a dedicated permission is not needed).
7. Token model and policy engine, in short#
Every McpAccessToken row points at one userLoginId and grants no
permission of its own; the agent acts as that user, and readOnly=Y
blocks every tool not classified read-only regardless of what the user
could otherwise do. McpPolicy then checks the call in a fixed order: a
hard denylist, the server’s own serviceDeny, MCP_GATEWAY for a direct
non-tool call, the service’s own declared permissions, then a component
rule (_VIEW/_UPDATE on the owning component’s webapp base). A tool’s
readOnly flag never widens the wrapped service’s own read/write
classification. Full token model, permission table and policy order:
Agent access security
.
8. The curl smoke test#
Run this after adding a server profile or a tool, before connecting a real
client. Create a token for a test user (Webtools “Agent Access” pages, or
createMcpAccessToken; the token value is shown once), then open a
session and list the tools, reading the Mcp-Session-Id response header
that every later call needs:
curl -s -k -X POST "https://localhost:8443/admin/mcp" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-H "MCP-Protocol-Version: 2025-06-18" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2025-06-18",
"capabilities":{},
"clientInfo":{"name":"smoke-test","version":"1.0"}}}' \
-D - -o /tmp/mcp-init.jsonList the tools with that session:
curl -s -k -X POST "https://localhost:8443/admin/mcp" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-H "Mcp-Session-Id: <session-id>" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'Call one read-only tool:
curl -s -k -X POST "https://localhost:8443/admin/mcp" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-H "Mcp-Session-Id: <session-id>" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call",
"params":{"name":"scipio_whoami","arguments":{}}}'Confirm HTTP 200, no stack trace, and a result that matches the token
user. A 404 on a later call means the session expired; return to the
initialize step. Then test a denied path: call a tool with a wrong or
expired token, and confirm the response reports a permission error, not a
server error.
9. Checklist before you ship a change#
- The new tool, server, or provider compiles as part of the component’s own build.
- Every write tool sets
readOnly = falseand states apermission, or relies on the webapp base permission on purpose. - A new permission is granted to
FULLADMIN, and toSCIPIO_AGENTonly when every agent should have it by default. - A new skill is 60 to 120 lines, packages through
./gradlew assembleAgentPlugin, and names real tool names only. - The curl smoke test in section 8 passes for an allowed and a denied call.
Scenario test suite#
tools/mcp-test.sh runs end-to-end scenarios against a running server
with curl only. The admin token must belong to a FULLADMIN user, the
customer token to a SCIPIO_CUSTOMER_AGENT shop customer, the agent token
to scp-agent; the exit code is the number of failed checks.
tools/mcp-test.sh -t <admin-token> -c <customer-token> -a <agent-token> all
tools/mcp-test.sh -t <admin-token> product # virtual product, two variants, prices, category
tools/mcp-test.sh -t <admin-token> -a <agent-token> security # fail-closed policy, proxy header, deny lists
tools/mcp-test.sh -t <admin-token> skills # every skill loads and names real toolsOther scenarios: user, order invoice (with -c), cms, cmstools (with -a).
Install a client, and the plugin build#
Webtools > Agent Access > Tokens and Skills cover the end-user connection
steps; see Agent quickstart
.
./gradlew assembleAgentPlugin builds the same downloadable zip from the
source tree for CI, packaging every Agent Skill and the MCP connection
config into one Claude Code plugin.
App-defined core tools#
Each application declares its core functionality in its @McpServer
profile: hand-written featured @McpTool methods, featured
@McpServiceTool entries for services that need custom names, and
featuredServices = { "createOrder", ... } (every featured service with
no explicit tool becomes one automatically). Agents discover them first in
the app’s own tools/list, through scipio_apps with action tools
(any endpoint, featured first), and through scipio_apps with action
call. scipio_apps with action list
lists every deployed webapp with its mcpUrl and coreTools names.