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

  1. Three Contracts, Not One
  2. What the MCP Skills Extension Standardizes
  3. Secure Loading Lifecycle
  4. Map MCP Tools into Eino
  5. Runnable Policy-Guarded Tool
  6. Build the Request-Scoped Agent
  7. Nested Skills and AgentTool
  8. Observability and Audit
  9. Testing Strategy
  10. Frequently Asked Questions
  11. 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:

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

  1. declares both resources and the Skills extension in server/discover
  2. implements skills/list
  3. implements skills/get
  4. serves each file through resources/read
  5. optionally implements resources/directory/read when directoryRead: 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:

text
(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."

flowchart TD A["Discover server capabilities"] --> B["skills/list or explicit URI"] B --> C["skills/get held entry"] C --> D["Show origin, purpose, and requested tools"] D --> E{"User and policy approve?"} E -->|"no"| F["Reject"] E -->|"yes"| G["Read required resources lazily"] G --> H["Verify bytes against held digests"] H --> I["Compile effective runtime policy"] I --> J["Execute with guarded tools"] J --> K["Audit result and resource versions"]

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.

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

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

  1. Authenticate the caller and resolve tenant policy.
  2. Load and verify the approved Skill entry and required resources.
  3. Parse requested tools from Skill instructions or frontmatter.
  4. Intersect them with host and user policy.
  5. Fetch current MCP tool descriptors for allowed servers.
  6. Namespace names that collide across servers.
  7. Wrap each call with the guard shown above.
  8. Create adk.ChatModelAgent with finite MaxIterations.
  9. Execute through adk.NewRunner with adk.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.NewAgentTool wraps 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:

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

Primary Sources