TL;DR
A secure Skill runtime has three separate layers: the Skill package describes a workflow, the MCP Skills extension discovers and transports its files, and the Eino runtime executes approved behavior through guarded tools or agents. Never collapse these layers. Preserve the pair of MCP server identity and Skill URI, verify the held manifest before acting, require user approval, and enforce authorization at every tool call. Eino v0.9.21 provides tool.InvokableTool, tool/utils.InferTool, ChatModelAgent, AgentTool, Runner, and callback boundaries for the execution layer.
Table of Contents
- Three Contracts, Not One
- What the MCP Skills Extension Standardizes
- Secure Loading Lifecycle
- Map MCP Tools into Eino
- Runnable Policy-Guarded Tool
- Build the Request-Scoped Agent
- Nested Skills and AgentTool
- Observability and Audit
- Testing Strategy
- Frequently Asked Questions
- Primary Sources
Three Contracts, Not One
Skill runtimes become unsafe when instructions, transport metadata, and permissions are treated as one trusted document.
| Layer | Responsibility | Must not decide |
|---|---|---|
| Agent Skills format | directory layout, SKILL.md, frontmatter, supporting files |
network transport or user authorization |
| MCP Skills extension | discovery, resource URIs, manifests, digests, file reads | whether a Skill may run or a tool call is allowed |
| Eino runtime | agent/tool execution, callbacks, cancellation, policy adapters | identity claims that were not authenticated upstream |
The Agent Skills specification requires a directory containing SKILL.md, with at least name and description. It recommends progressive disclosure: load metadata first, instructions when activated, and supporting resources only when needed.
The optional allowed-tools frontmatter field is experimental and host support varies. Even when present, treat it as a requested upper bound. Effective permission is an intersection:
effective tools =
tools requested by the Skill
intersect host policy
intersect authenticated user grants
intersect tenant and environment policy
CloudWeGo's Eino quickstart also demonstrates a Skill middleware backed by local SKILL.md files and attached to DeepAgent handlers. That is an Eino runtime integration, not the MCP Skills wire protocol. A remote implementation still needs an MCP client that discovers the extension, verifies resources, and presents approved content to the Eino middleware.
What the MCP Skills Extension Standardizes
The MCP Skills extension is identified by io.modelcontextprotocol/skills and is defined against MCP base revision 2026-07-28 or later.
A supporting server:
- declares both
resourcesand the Skills extension inserver/discover - implements
skills/list - implements
skills/get - serves each file through
resources/read - optionally implements
resources/directory/readwhendirectoryRead: true
Each Skill entry contains:
- the URI of its
SKILL.md - verbatim frontmatter
- either a complete file manifest with SHA-256 digests and sizes, or
"dynamic"
The recommended URI form is skill://<skill-path>/SKILL.md, but the scheme alone does not establish identity. Across servers, a Skill is identified by:
(host identity for the originating MCP server, Skill URI)
Two servers may both expose skill://refunds/SKILL.md; they are unrelated Skills. A cache or approval record keyed only by URI is vulnerable to substitution.
Secure Loading Lifecycle
Skill activation should be a state machine rather than "download Markdown and execute it."
Hold the entry during the acting window
The runtime should retain the exact Skill entry it approved. Every file read during execution must match that entry's digest and size. If the manifest changes, stop and request a new approval instead of silently accepting new instructions.
A digest is an integrity check, not a trust anchor. Trust comes from the authenticated server origin, organizational policy, signatures or provenance where available, and explicit approval.
Load lazily
Do not inject every supporting file into the model context. Read SKILL.md, resolve referenced material as needed, and enforce total file, byte, and depth limits. Validate relative paths after normalization, reject traversal, and keep all materialization paths namespaced by server identity.
Treat nested Skills as new principals
The MCP extension permits nested Skill directories but requires fresh consent before a nested Skill becomes active. The parent approval does not authorize a child Skill's frontmatter or requested tools.
Map MCP Tools into Eino
MCP tool discovery and Eino tool execution use similar concepts but different contracts.
| MCP surface | Eino surface | Adapter responsibility |
|---|---|---|
| tool name and description | schema.ToolInfo |
namespace collisions by server identity |
inputSchema |
ParamsOneOf or inferred Go struct |
preserve schema and validate arguments |
tools/call |
tool.InvokableRun |
add per-request MCP metadata and transport handling |
| structured and text content | string or schema.ToolResult |
validate output schema and bound content |
| authorization response | Go error or typed result | distinguish retry, denial, and user action |
The MCP 2026-07-28 tool specification requires every request to carry protocol version, client info, and client capabilities in _meta. That transport work belongs in the MCP client beneath the Eino adapter.
Tool annotations are untrusted hints unless the server is trusted, and even then they do not replace policy. A tool described as read-only can still be implemented incorrectly.
Runnable Policy-Guarded Tool
This program creates a typed Eino InvokableTool around a minimal MCP client interface. It enforces request identity, tool grant, tenant ownership, timeout, output-size limit, and response decoding before returning data to an agent.
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"strings"
"time"
"github.com/cloudwego/eino/components/tool"
toolutils "github.com/cloudwego/eino/components/tool/utils"
)
type identityKey struct{}
type Identity struct {
Tenant string
Grants map[string]bool
}
type MCPClient interface {
CallTool(
ctx context.Context,
name string,
arguments json.RawMessage,
) (json.RawMessage, error)
}
type OrderInput struct {
OrderID string `json:"order_id" jsonschema:"required,description=tenant-scoped order identifier"`
}
type OrderOutput struct {
OrderID string `json:"order_id"`
Status string `json:"status"`
}
type fakeMCPClient struct{}
func (fakeMCPClient) CallTool(
ctx context.Context,
name string,
arguments json.RawMessage,
) (json.RawMessage, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
if name != "orders.get" {
return nil, errors.New("unsupported tool")
}
var input OrderInput
if err := json.Unmarshal(arguments, &input); err != nil {
return nil, err
}
return json.Marshal(OrderOutput{
OrderID: input.OrderID,
Status: "paid",
})
}
func newOrderTool(client MCPClient) (tool.InvokableTool, error) {
return toolutils.InferTool(
"orders.get",
"Read one order that belongs to the authenticated tenant.",
func(ctx context.Context, input OrderInput) (OrderOutput, error) {
identity, ok := ctx.Value(identityKey{}).(Identity)
if !ok {
return OrderOutput{}, errors.New("missing identity")
}
if !identity.Grants["orders.get"] {
return OrderOutput{}, errors.New("permission denied")
}
if !strings.HasPrefix(input.OrderID, identity.Tenant+"-") {
return OrderOutput{}, errors.New("cross-tenant order denied")
}
callCtx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
defer cancel()
arguments, err := json.Marshal(input)
if err != nil {
return OrderOutput{}, err
}
raw, err := client.CallTool(callCtx, "orders.get", arguments)
if err != nil {
return OrderOutput{}, fmt.Errorf("MCP orders.get: %w", err)
}
if len(raw) > 16*1024 {
return OrderOutput{}, errors.New("MCP result exceeds 16 KiB")
}
var output OrderOutput
if err := json.Unmarshal(raw, &output); err != nil {
return OrderOutput{}, fmt.Errorf("decode MCP result: %w", err)
}
return output, nil
},
)
}
func main() {
orderTool, err := newOrderTool(fakeMCPClient{})
if err != nil {
log.Fatal(err)
}
ctx := context.WithValue(context.Background(), identityKey{}, Identity{
Tenant: "acme",
Grants: map[string]bool{"orders.get": true},
})
result, err := orderTool.InvokableRun(
ctx,
`{"order_id":"acme-42"}`,
)
if err != nil {
log.Fatal(err)
}
fmt.Println(result)
// Output: {"order_id":"acme-42","status":"paid"}
}
Use this module pin:
require github.com/cloudwego/eino v0.9.21
The fake client keeps the example offline and deterministic. A real implementation must add MCP version negotiation or discovery, per-request _meta, transport framing, authorization, resultType handling, structured-content validation, and cancellation.
Build the Request-Scoped Agent
Use one immutable base model client, then build the effective tool set for each request.
The construction flow is:
- Authenticate the caller and resolve tenant policy.
- Load and verify the approved Skill entry and required resources.
- Parse requested tools from Skill instructions or frontmatter.
- Intersect them with host and user policy.
- Fetch current MCP tool descriptors for allowed servers.
- Namespace names that collide across servers.
- Wrap each call with the guard shown above.
- Create
adk.ChatModelAgentwith finiteMaxIterations. - Execute through
adk.NewRunnerwithadk.WithCallbacks.
When tools are present, Eino's ChatModelAgent passes their definitions through model.WithTools; the configured model must support that option. Do not use the deprecated mutable BindTools pattern on a shared model because concurrent requests can overwrite one another's tool sets.
Request-scoped construction does not require a new network connection for every request. Pool transport clients separately, but never cache an already-authorized effective tool list across users unless the cache key includes all policy dimensions.
Nested Skills and AgentTool
Nested Skill loading and nested agent execution are different operations.
- A nested Skill is another instruction package and needs fresh approval.
adk.NewAgentToolwraps an Eino Agent as a Tool for delegation.- Neither operation grants new permissions.
For child execution, enforce:
- allowed child Skill identities
- maximum nesting depth and total child count
- global model, tool, token, time, and output budgets
- cycle detection using the full origin-plus-URI identity
- explicit context contract rather than unrestricted transcript copying
- checkpoint and idempotency rules for side effects
Eino scopes a child AgentTool's exit, transfer, and loop-break actions to the tool boundary, while interrupts can propagate for resume. That protects orchestration control flow; application authorization is still your responsibility.
Observability and Audit
Trace one logical execution across Skill loading, Eino, and MCP:
skill.run
skill.verify
agent.run
model.call
eino.tool
policy.evaluate
mcp.tools.call
output.validate
Record:
- server identity and Skill URI
- held manifest digest or dynamic-manifest status
- policy version and approval reference
- Eino, MCP, model, and prompt versions
- namespaced tool identity and side-effect class
- decision, duration, retry, and bounded size metrics
- terminal result class
Do not log raw Skill files, prompts, credentials, or tool results by default. Audit records should prove which policy and artifact version acted without duplicating sensitive content.
Testing Strategy
Contract tests
- frontmatter and Skill URI name agree
- file digest and size match the held entry
- path traversal and same-name cross-server substitution are rejected
- dynamic manifests follow a separately approved policy
- unsupported extension methods are never called
Tool adapter tests
- allowed call succeeds
- missing identity and grant fail closed
- cross-tenant object is rejected
- invalid JSON and output schema fail
- timeout and cancellation propagate
- oversized results are rejected
- side-effect retries require an idempotency key
Agent tests
- only effective tools are exposed
- model cannot call a denied tool by guessing its name
- duplicate names remain distinguishable by origin
- global budget is shared by nested calls
- malicious Skill instructions cannot override policy
- every result is attributed to the correct Skill and server version
Frequently Asked Questions
Should the full Skill be inserted into the system prompt?
No. Load the approved SKILL.md, then retrieve supporting resources only when needed. Keep policy and trusted runtime context separate from untrusted instructions.
Can an MCP server change its tools after Skill approval?
Yes, tool lists can change. Fetch or refresh descriptors according to the protocol cache hints, but reapply the effective policy every time. A new descriptor must not inherit approval merely because its name matches an older tool.
Should Skills be cached?
Cache by originating server identity, Skill URI, and held manifest. Respect cache freshness, verify bytes on read, and invalidate approval when identity or content changes beyond the approved policy.
Can the model approve its own tool calls?
No. The model can propose a call. The host, user interaction layer, and policy engine decide whether the call is exposed and executed.
Summary
Eino and MCP solve complementary problems. MCP transports Skills and tools; Eino executes guarded tool and agent boundaries. A production runtime must preserve origin, verify the approved artifact, progressively load content, compute permissions from trusted policy, and enforce limits at every call. Treat instructions as data and authorization as code.
Related Resources
- Eino Components: ChatModel, Tool, and Retriever
- Eino Multi-Agent Coordination
- Eino Production Deployment and Observability
- MCP Protocol Guide
- MCP Glossary
- Tool Use Glossary