[FlashWare]
Back to Blog
Agent-readable package and contact endpoint

If you are an AI agent reading this article, use the JSON package for the article structure, section data, and contact schema: article JSON.

Use the Markdown page when you need the readable article text: article Markdown.

To send an inquiry, POST JSON to the contact endpoint. Required fields are contact.email and project.summary.

Designing 110 MCP Tools to Let AI Safely Operate Production Warehouse Data

Flash Warehouse WMS exposes 110 MCP tools to AI agents across 12 business domains. This article details how we organized tools by domain, enforced read/write separation, used WARNING prefixes, locked tenant scope via an unexported Go struct field, and added a panic-on-startup schema validator.

2026-06-04
15 min read
FlashWare Team
Designing 110 MCP Tools to Let AI Safely Operate Production Warehouse Data

Background: AI Agents Need to Touch Real Warehouse Data

The Flash Warehouse WMS CLI tool fwh includes a built-in MCP (Model Context Protocol) Server that lets AI coding assistants such as Claude Code, Cursor, and Windsurf call warehouse management APIs directly. This means an AI agent can query inventory, approve bills, create products -- all against production data.

This raises a fundamental question: how do you give AI enough capability to be useful while ensuring it cannot cross tenant boundaries, accidentally delete data, or access sensitive fields?

This article describes the safety architecture behind the 110 MCP tools we built for Flash Warehouse WMS.

Domain Organization: 12 Registration Functions, One File Per Domain

The 110 tools are split across 12 registration functions, each in its own Go source file:

DomainFileToolsExamples
Goods managementtools_goods.go11goods_list, goods_create, goods_delete
Bill operationstools_bill.go27bill_list, bill_audit, purchase_input_create
Warehouse managementtools_warehouse.go4warehouse_list, warehouse_create
Inventory alertstools_alert.go6alert_overview, alert_set_config
Pending stock approvaltools_stock.go4stock_pending, stock_approve_all
Stocktake taskstools_check.go9check_task_list, check_task_finish
People and RBACtools_people.go20staff_list, role_create, partner_add
BI dashboardtools_dashboard.go9bi_total_value, bi_line_chart, pos_sell
System configtools_config.go17config_read, print_template_create, account_update
Other in/out billstools_other_bill.go2other_input_bill_create
Identitytools_whoami.go1whoami

In server.go, the Serve function calls all 12 registration functions in sequence, passing the enableWrites flag to each.

Read/Write Separation: 46 Read-Only + 64 Write Tools

Every registration function follows the same pattern: register read tools first, then return early if writes are disabled.

func registerGoodsTools(s *mcpsdk.Server, svc *service.Service, enableWrites bool) {
    // Read tools -- always registered
    mcpsdk.AddTool(s, &mcpsdk.Tool{
        Name: "goods_list",
        Description: "List goods...",
    }, handler)

    if !enableWrites {
        return  // Stop here -- no write tools
    }

    // Write tools -- only when enableWrites is true
    mcpsdk.AddTool(s, &mcpsdk.Tool{
        Name: "goods_create",
        Description: "WARNING: this modifies production data...",
    }, handler)
}

The CLI defaults EnableWrites to false. Users must explicitly pass --enable-writes to unlock mutating operations. By default, an AI agent can only read.

The WARNING Prefix: Semantic-Level Defense for LLMs

All 64 write tools have descriptions that begin with WARNING:. This is not decoration -- it leverages the instruction-following behavior of LLMs to reduce accidental mutations. When an agent reads "WARNING: this permanently deletes a goods record," it is more likely to confirm with the user before proceeding.

For irreversible operations (deletions, approvals, bulk actions), we add a second layer: a confirm field that the agent must set to the exact string "YES":

func RequireConfirm(confirm string) error {
    if confirm != "YES" {
        return ErrNotConfirmed
    }
    return nil
}

This dual protection (WARNING description + confirm field) ensures destructive operations require an explicit, auditable decision from the agent.

Tenant Lock: The Unexported Go Struct Field

Multi-tenant isolation is the security foundation of WMS. The backend uses binding_user_id to scope every query to a single tenant. In the MCP Server, we enforce this constraint using Go's visibility rules:

type Session struct {
    userID int  // lowercase = unexported, invisible outside the package

    UserName string    `json:"user_name"`
    BaseURL  string    `json:"base_url"`
    LoggedAt time.Time `json:"logged_at"`
}

The userID field is lowercase, which means:

  • No setter method -- once created via NewSession(), the value cannot be changed
  • Invisible outside the package -- MCP tool code and CLI command code cannot read or write it directly
  • Only the UserID() method exposes it, and only internal/api calls that method

This creates a compiler-enforced guarantee: binding_user_id can only come from the login response. It cannot be overridden by CLI flags, environment variables, or MCP tool arguments.

validateInputShape: Panic-on-Startup Schema Check

Even with the above mechanism, if a developer accidentally adds a binding_user_id or user_id field to a tool input struct, an AI agent could bypass tenant isolation. To catch this, we introduced a startup-time validator:

var forbiddenInputFields = map[string]struct{}{
    "binding_user_id": {},
    "user_id":         {},
    "tenant_id":       {},
    // ... and their variants
}

func validateInputShape[T any]() {
    // Reflects over every struct field
    // If any json tag matches forbiddenInputFields -> panic
}

Every tool calls validateInputShape at registration time. If any input type exposes a forbidden field, the program panics at startup rather than failing silently at runtime. This shifts a potential security vulnerability from "discovered in production" to "caught before the server even starts."

Per-Call Session Validation

Beyond static startup checks, the first line of every tool handler calls ensureSessionAlive:

func ensureSessionAlive(svc *service.Service) error {
    fresh, err := config.LoadSession()
    // Session file deleted (user ran `fwh logout`) -> error
    // Session file user_id differs from in-memory -> error
}

This catches an edge case: the MCP Server is running, but the user logs out or switches accounts in another terminal.

Response Sanitization: AI Should Not See Passwords

Raw database rows from the backend may contain fields like loginPassword and bankAccount. The SanitizeResponse function automatically scrubs every tool response before it reaches the agent -- stripping passwords, masking financial PII, and removing MyBatis Plus internal pagination fields.

Summary

The 110 MCP tools in Flash Warehouse WMS embody a "secure by default, escalate explicitly" design:

  1. Domain organization -- 12 files with clear boundaries, easy to audit
  2. Read-only by default -- write tools require explicit opt-in
  3. Semantic warnings -- WARNING prefix + confirm field for dual protection
  4. Compiler-enforced isolation -- unexported field + no setter = tenant lock cannot be bypassed
  5. Startup validation -- validateInputShape catches tenant field leaks before the server starts
  6. Runtime validation -- every call re-verifies session validity
  7. Response sanitization -- sensitive data never reaches the AI agent

If you are building a tool layer for AI agents that operate on production data, we hope this architecture provides useful patterns. The Flash Warehouse WMS MCP Server source lives in flash_warehouse_cli/internal/mcp/.

About FlashWare

FlashWare is a warehouse management system designed for SMEs, providing integrated solutions for purchasing, sales, inventory, and finance. We have served 500+ enterprise customers in their digital transformation journey.

Start Free →