{"id":237,"date":"2026-06-19T19:09:44","date_gmt":"2026-06-19T11:09:44","guid":{"rendered":"https:\/\/erishen.cn\/?page_id=237"},"modified":"2026-06-19T19:27:02","modified_gmt":"2026-06-19T11:27:02","slug":"building-ai-tool-server-lobster-architecture","status":"publish","type":"page","link":"https:\/\/erishen.cn\/?page_id=237","title":{"rendered":"Building a Tool Server for Your AI Assistant: Lobster&#8217;s Architecture"},"content":{"rendered":"<p><\/p>\n<p>AI assistants can talk, but they can&#8217;t do things. Everyone knows this problem. OpenAI provided a solution with the Function Calling protocol \u2014 letting LLMs recognize which tools to call during a conversation and having an external service execute them. But the protocol only solves half the problem: who provides these tools? Who executes them? Who guarantees safety?<\/p>\n<p><\/p>\n<p>I built a tool server for my AI assistant (OpenClaw, a.k.a. &#8220;Lobster&#8221;) called Lobster. It runs on a cloud server, and Lobster calls Lobster&#8217;s API through Function Calling, transforming from &#8220;just a talker&#8221; to &#8220;someone who gets things done.&#8221; This article isn&#8217;t about Lobster&#8217;s specific tool list \u2014 it&#8217;s about the architectural patterns: the ToolRegistry pattern, Function Calling bridging, security layering, observability, and plugin extension. You can reuse these patterns to build a tool server for any AI assistant.<\/p>\n<p><\/p>\n<h2>Problem 1: How Does the AI &#8220;See&#8221; Your Tools?<\/h2>\n<p><\/p>\n<p>Before an AI assistant can call a tool, it needs to know what tools are available and what parameters each requires. OpenAI defined the Function Calling format \u2014 a JSON Schema containing the tool name, description, and parameter definitions. But your tools are Python functions, CLI commands, HTTP requests \u2014 all in different formats. How do you unify them into a standard format the AI can understand?<\/p>\n<p><\/p>\n<p>Lobster&#8217;s approach is the ToolRegistry. The core is simple: when registering each tool, you provide four things \u2014 name, description, parameter definition (JSON Schema), and execution function. Once registered, the ToolRegistry automatically converts all tools into OpenAI Function Calling format.<\/p>\n<p><\/p>\n<pre><code>\nregistry.register(Tool(\n    name=\"stock_quote\",\n    description=\"Get real-time stock quote\",\n    parameters={\n        \"type\": \"object\",\n        \"properties\": {\n            \"code\": {\"type\": \"string\", \"description\": \"Stock code (e.g. sh600519, sz000001, hk00700)\"},\n        },\n        \"required\": [\"code\"]\n    },\n    handler=lambda code: investment_tools.get_stock_quote(code),\n    category=\"investment\"\n))\n<\/code><\/pre>\n<p><\/p>\n<p>After registration, the AI assistant gets the tool list with a single HTTP request:<\/p>\n<p><\/p>\n<pre><code>\ncurl http:\/\/localhost:8000\/tools\/openai\n<\/code><\/pre>\n<p><\/p>\n<p>The response is standard Function Calling JSON that can be fed directly to any LLM supporting function calls \u2014 GPT, Claude, DeepSeek, local Ollama models, all of them.<\/p>\n<p><\/p>\n<pre><code>\n{\n  \"type\": \"function\",\n  \"function\": {\n    \"name\": \"stock_quote\",\n    \"description\": \"Get real-time stock quote\",\n    \"parameters\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"code\": {\"type\": \"string\", \"description\": \"Stock code (e.g. sh600519, sz000001, hk00700)\"}\n      },\n      \"required\": [\"code\"]\n    }\n  }\n}\n<\/code><\/pre>\n<p><\/p>\n<p>The beauty of this pattern: your tool implementation is a Python function, but the format exposed to the AI is standardized. Adding a new tool is just one <code>register()<\/code> call \u2014 no format conversion worries. The ToolRegistry handles mapping from registration info to Function Calling JSON; you just focus on the business logic.<\/p>\n<p><\/p>\n<h2>Problem 2: How Does the AI &#8220;Call&#8221; Your Tools?<\/h2>\n<p><\/p>\n<p>Once the AI assistant knows the tool list and decides during conversation that it needs stock data, it returns a function call instruction:<\/p>\n<p><\/p>\n<pre><code>\n{\"name\": \"stock_quote\", \"arguments\": {\"code\": \"sh600519\"}}\n<\/code><\/pre>\n<p><\/p>\n<p>Your server needs to receive this instruction, find the corresponding handler, execute it, and return the result. Lobster uses FastAPI to build an API server where each registered tool is automatically mapped to an execution endpoint:<\/p>\n<p><\/p>\n<pre><code>\ncurl -X POST http:\/\/localhost:8000\/tools\/stock_quote\/execute \\\n  -H \"Content-Type: application\/json\" \\\n  -d '{\"code\": \"sh600519\"}'\n<\/code><\/pre>\n<p><\/p>\n<p>The ToolRegistry does three things upon receiving a request: looks up the registered handler, executes it, and returns the result. Lookup is O(1) by name index, execution is a direct Python function call, and the return is in unified JSON format.<\/p>\n<p><\/p>\n<p>The complete interaction chain: LLM identifies intent \u2192 generates function call \u2192 Lobster API receives it \u2192 ToolRegistry looks up and executes \u2192 result returned to LLM \u2192 LLM organizes a response using real data. Throughout the process, the user only converses with the AI assistant \u2014 tool calls are transparent.<\/p>\n<p><\/p>\n<p>A design choice here: your API server could also expose just a single unified <code>\/execute<\/code> endpoint without routing by tool name. Lobster chose per-tool-name routing (<code>\/tools\/{tool_name}\/execute<\/code>) because it makes per-tool API call logs cleaner \u2014 debugging instantly reveals which tool was called. If you don&#8217;t have many tools, a unified endpoint works fine too.<\/p>\n<p><\/p>\n<h2>Problem 3: Letting AI Do Things Without Letting It Do Anything Reckless<\/h2>\n<p><\/p>\n<p>This is the most easily overlooked problem. Your AI assistant can now read\/write files, execute commands, make HTTP requests \u2014 which means it can also accidentally delete files, run dangerous commands, or request internal network addresses. When giving AI execution capabilities on a cloud server, the security boundary is non-negotiable.<\/p>\n<p><\/p>\n<p>Lobster adds a security validation layer before every tool execution, across three dimensions:<\/p>\n<p><\/p>\n<p><strong>Path Security.<\/strong> File operation tools like <code>file_read<\/code> and <code>file_write<\/code> can only access allowed directories. By default, they&#8217;re limited to the current working directory. You can extend the allowed scope through configuration, but the AI cannot escape the boundary to read <code>\/etc\/passwd<\/code> or write <code>\/root\/.bashrc<\/code>.<\/p>\n<p><\/p>\n<p><strong>Command Security.<\/strong> The <code>run_shell<\/code> tool has a blocklist: <code>rm -rf \/<\/code>, <code>mkfs<\/code>, <code>dd if=<\/code>, fork bombs, <code>chmod 777 \/<\/code>, <code>wget<\/code> \u2014 these dangerous commands are intercepted directly. You can also configure an allowlist mode that only permits pre-approved safe commands.<\/p>\n<p><\/p>\n<p><strong>URL Security.<\/strong> <code>http_get<\/code> and <code>http_post<\/code> only allow http\/https protocols and block localhost, 127.0.0.1, and 0.0.0.0 \u2014 preventing the AI from being tricked into requesting internal services on your server (SSRF protection).<\/p>\n<p><\/p>\n<p>There&#8217;s also a special check for Python code: the <code>run_python<\/code> tool scans for five high-risk patterns before execution \u2014 <code>os.system<\/code>, <code>subprocess<\/code>, <code>eval()<\/code>, <code>exec()<\/code>, <code><strong>import<\/strong><\/code> \u2014 and refuses to execute if any match is found. The calculator tool uses <code>ast.parse<\/code> + <code>operator<\/code> mapping instead of <code>eval()<\/code>, allowing only arithmetic operations.<\/p>\n<p><\/p>\n<p>These validations aren&#8217;t decorative optional layers \u2014 they&#8217;re mandatory checkpoints for every tool execution. The more capable your AI assistant becomes, the more you need to constrain what it can do. Security validation and tool capability should grow together \u2014 for every new tool you add, think about how it could be abused, then add corresponding protections.<\/p>\n<p><\/p>\n<h2>Problem 4: How Do You Know What the AI Called and How It Went?<\/h2>\n<p><\/p>\n<p>Your tool server is running on the cloud \u2014 you&#8217;re not there watching it. Which tools did the AI call? Which one was the slowest? Which has the highest failure rate? You need to be able to see this information.<\/p>\n<p><\/p>\n<p>Lobster implements two layers of observability:<\/p>\n<p><\/p>\n<p><strong>Execution Statistics.<\/strong> Every tool call is recorded to <code>~\/.lobster\/stats\/<\/code>, persisted across sessions. Statistics include: total call count, success\/failure counts, average execution time, and recent call history. You can see the most-used tool rankings, the slowest calls, and the tools with the highest error rates. This data helps you decide which tools are worth optimizing and which might have issues.<\/p>\n<p><\/p>\n<p><strong>Caching.<\/strong> The ToolRegistry has a built-in LRU cache with TTL expiration. The same stock code won&#8217;t trigger repeated requests to the Sina Finance API within one minute \u2014 because market data doesn&#8217;t change much in 60 seconds, and cache hit rates directly reduce external API call volume. The cache also tracks statistics: hit rate, cache size, and eviction count.<\/p>\n<p><\/p>\n<p>These two layers are complementary: statistics tell you macro trends (which tools are used most, which are slow), and caching gives you micro optimizations (reducing duplicate calls, lowering latency). If you&#8217;re building your own tool server, both layers are worth implementing \u2014 it doesn&#8217;t need to be complex. A JSON file for stats + an OrderedDict for caching is enough to get started.<\/p>\n<p><\/p>\n<h2>Problem 5: How to Add New Tools<\/h2>\n<p><\/p>\n<p>Your AI assistant&#8217;s needs will keep changing. Today it only needs stock quotes, tomorrow it might need to read emails, and the day after it might need to call your company&#8217;s internal API. The tool server can&#8217;t be a closed system \u2014 it needs a low-cost extension mechanism.<\/p>\n<p><\/p>\n<p>Lobster provides two layers of extension:<\/p>\n<p><\/p>\n<p><strong>Registration Extension.<\/strong> Adding a new tool is a single line of <code>registry.register()<\/code>. You write a handler function in any Python module, call register, and the ToolRegistry automatically includes it in the Function Calling format. No need to modify API server code, no need to manually write JSON Schema \u2014 the parameter definitions provided at registration are automatically converted.<\/p>\n<p><\/p>\n<p><strong>Plugin Extension.<\/strong> <code>~\/.lobster\/plugins\/*\/plugin.py<\/code> is auto-loaded. You create a directory, put a plugin.py file in it, define your tools, and the next time Lobster starts, they&#8217;re automatically discovered and registered. A template generator helps you quickly create plugin skeletons.<\/p>\n<p><\/p>\n<p>The difference between these two layers: registration extension is for tools you develop yourself (you know where the code is, just import it directly). Plugin extension is for tools contributed by others or tools you want to isolate (independent directory, not coupled to the main code). Your project might only need registration extension, but retaining the plugin mechanism means you can add third-party tools with zero cost in the future.<\/p>\n<p><\/p>\n<h2>About MCP<\/h2>\n<p><\/p>\n<p>You may have noticed that Anthropic has introduced MCP (Model Context Protocol), which is becoming a new trend in tool integration. Cursor, VS Code, and various agent frameworks are adopting MCP. What&#8217;s the relationship with Function Calling?<\/p>\n<p><\/p>\n<p>MCP and Function Calling solve the same problem \u2014 letting AI call external tools. But the scenarios differ. MCP is designed for IDE and local tool integration \u2014 AI agents connect to local tool servers through the MCP protocol, suited for development scenarios. Function Calling is designed for cloud AI assistants \u2014 LLMs generate tool call instructions in the conversation flow, executed by your server, suited for assistants deployed in the cloud.<\/p>\n<p><\/p>\n<p>Lobster uses the Function Calling format because it runs on a cloud server, and Lobster is also in the cloud. That&#8217;s the right choice. But if you&#8217;re doing IDE integration or local agents, MCP is the more appropriate protocol.<\/p>\n<p><\/p>\n<p>The good news: the ToolRegistry pattern is protocol-agnostic. Your tool registration, security validation, caching, and statistics architecture layers don&#8217;t depend on the specific Function Calling format. Switching to MCP, you only need to change one layer \u2014 from Function Calling JSON to MCP Tool definition format \u2014 while all the handlers, security validation, caching, and statistics underneath are fully reusable. The architectural thinking is universal; the protocol format is replaceable.<\/p>\n<p><\/p>\n<h2>Architecture Summary<\/h2>\n<p><\/p>\n<p>Tying the five problems above together, Lobster&#8217;s architecture looks like this:<\/p>\n<p><\/p>\n<p>The <strong>Tool Registration Layer<\/strong> (ToolRegistry) centrally manages metadata and execution functions for all tools, automatically converting them into a standard format LLMs can understand. The <strong>API Server Layer<\/strong> (FastAPI) maps each registered tool to an HTTP endpoint, allowing the AI assistant to call them over the network. The <strong>Security Validation Layer<\/strong> performs three-dimensional checks (path, command, URL) before every execution, ensuring the AI can do things without doing reckless things. The <strong>Observability Layer<\/strong> records execution statistics and cache data, giving you control over every tool call the AI makes. The <strong>Extension Layer<\/strong> adds new tools through both registration and plugin mechanisms, ensuring the toolset can continuously grow.<\/p>\n<p><\/p>\n<p>These five layers are each independent \u2014 you can adopt them as needed. If you only need the minimal setup: one ToolRegistry + one FastAPI server is enough. Security validation and caching can be added later. The plugin system can be skipped. What matters first is getting the &#8220;AI call \u2192 tool execution \u2192 result return&#8221; chain working, then gradually hardening it.<\/p>\n<p><\/p>\n<p>Building a tool server for an AI assistant \u2014 the core isn&#8217;t the tools themselves, but the architecture. Your toolset will change \u2014 today it&#8217;s stocks and code analysis, tomorrow it might be email and calendar. But the ToolRegistry pattern, security layering, observability, and extension mechanisms \u2014 these architectural layers hold steady regardless of how your tools evolve.<\/p>\n<p><\/p>\n<h2>Source Code Navigation<\/h2>\n<p><\/p>\n<table>\n<thead>\n<tr>\n<td>Architecture Layer<\/td>\n<td>Source File<\/td>\n<td>Description<\/td>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Tool Registration<\/td>\n<td><a href=\"https:\/\/github.com\/erishen\/lobster\/blob\/main\/src\/lobster\/core\/tools.py\">tools.py<\/a><\/td>\n<td>ToolRegistry, 14 built-in tools, security validation<\/td>\n<\/tr>\n<tr>\n<td>API Server<\/td>\n<td><a href=\"https:\/\/github.com\/erishen\/lobster\/blob\/main\/src\/lobster\/commands\/api_cmd.py\">api_cmd.py<\/a><\/td>\n<td>FastAPI endpoints, OpenAI Function Calling format<\/td>\n<\/tr>\n<tr>\n<td>Security Validation<\/td>\n<td><a href=\"https:\/\/github.com\/erishen\/lobster\/blob\/main\/src\/lobster\/core\/tools.py\">tools.py<\/a><\/td>\n<td>validate_path \/ validate_command \/ validate_url \/ _check_python_dangerous<\/td>\n<\/tr>\n<tr>\n<td>Execution Statistics<\/td>\n<td><a href=\"https:\/\/github.com\/erishen\/lobster\/blob\/main\/src\/lobster\/core\/stats.py\">stats.py<\/a><\/td>\n<td>ToolStatsTracker, persisted statistics<\/td>\n<\/tr>\n<tr>\n<td>Caching<\/td>\n<td><a href=\"https:\/\/github.com\/erishen\/lobster\/blob\/main\/src\/lobster\/core\/cache.py\">cache.py<\/a><\/td>\n<td>LRU cache, TTL expiration, hit rate stats<\/td>\n<\/tr>\n<tr>\n<td>Financial Tools<\/td>\n<td><a href=\"https:\/\/github.com\/erishen\/lobster\/blob\/main\/src\/lobster\/core\/investment.py\">investment.py<\/a><\/td>\n<td>stock_quote and investment tool registration<\/td>\n<\/tr>\n<tr>\n<td>Serena Integration<\/td>\n<td><a href=\"https:\/\/github.com\/erishen\/lobster\/blob\/main\/src\/lobster\/core\/serena_client.py\">serena_client.py<\/a><\/td>\n<td>Code intelligence framework integration<\/td>\n<\/tr>\n<tr>\n<td>Memory Store<\/td>\n<td><a href=\"https:\/\/github.com\/erishen\/lobster\/blob\/main\/src\/lobster\/core\/memory_store.py\">memory_store.py<\/a><\/td>\n<td>TF-IDF + Levenshtein fuzzy search<\/td>\n<\/tr>\n<tr>\n<td>Plugin System<\/td>\n<td><a href=\"https:\/\/github.com\/erishen\/lobster\/blob\/main\/src\/lobster\/core\/plugin.py\">plugin.py<\/a><\/td>\n<td>Auto-discovery and loading of custom plugins<\/td>\n<\/tr>\n<tr>\n<td>Configuration<\/td>\n<td><a href=\"https:\/\/github.com\/erishen\/lobster\/blob\/main\/src\/lobster\/core\/config.py\">config.py<\/a><\/td>\n<td>.env loading, LobsterConfig<\/td>\n<\/tr>\n<tr>\n<td>LLM Client<\/td>\n<td><a href=\"https:\/\/github.com\/erishen\/lobster\/blob\/main\/src\/lobster\/core\/llm_client.py\">llm_client.py<\/a><\/td>\n<td>EnhancedLLMClient, conversation management, caching, retry<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><\/p>\n<p>Project repository: <a href=\"https:\/\/github.com\/erishen\/lobster\">https:\/\/github.com\/erishen\/lobster<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>AI assistants can talk, but they can&#8217;t do things. [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"english_url":"","chinese_url":"https:\/\/erishen.cn\/?p=230","footnotes":""},"class_list":["post-237","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/erishen.cn\/index.php?rest_route=\/wp\/v2\/pages\/237","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/erishen.cn\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/erishen.cn\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/erishen.cn\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/erishen.cn\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=237"}],"version-history":[{"count":3,"href":"https:\/\/erishen.cn\/index.php?rest_route=\/wp\/v2\/pages\/237\/revisions"}],"predecessor-version":[{"id":253,"href":"https:\/\/erishen.cn\/index.php?rest_route=\/wp\/v2\/pages\/237\/revisions\/253"}],"wp:attachment":[{"href":"https:\/\/erishen.cn\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=237"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}