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.

java
@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.

java
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.

xml
<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:

bash
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.json

List the tools with that session:

bash
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:

bash
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 = false and states a permission, or relies on the webapp base permission on purpose.
  • A new permission is granted to FULLADMIN, and to SCIPIO_AGENT only 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.

text
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 tools

Other 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.

Ask the people who wrote it.

Support, development and training from the team that builds Scipio ERP.