MCP Servers, Plugins and External Tool Integration
Advanced⏱45 min📖4 min readLesson 4 of 5

MCP Servers, Plugins and External Tool Integration

Explore MCP server setup, external API integration, plugin architecture, tool permission scopes, and custom MCP implementations.

What is MCP?

The Model Context Protocol (MCP) is an open standard that defines how LLM applications communicate with external tools and data sources. It uses JSON-RPC 2.0 as its wire protocol.

ℹ️Note

MCP was designed specifically for LLM-tool interaction patterns. Unlike REST APIs which are designed for humans and CRUD operations, MCP uses a bidirectional JSON-RPC protocol that supports tool discovery, resource access, and prompt templates: all primitives that LLMs naturally understand.

100%

MCP JSON-RPC Protocol Exchange

Every interaction between OpenCode and an MCP server follows a structured JSON-RPC 2.0 conversation. Understanding this protocol is essential for debugging and building custom MCP servers.

100%
💡Tip

When debugging MCP issues, enable verbose logging to see the raw JSON-RPC messages. This is invaluable for identifying malformed tool schemas, incorrect response formats, or authentication failures.

MCP Server Lifecycle

100%

MCP Server Configuration

MCP servers are configured in the mcpServers section of opencode.json:

json
{ "mcpServers": { "filesystem": { "command": "npx", "args": [ "-y", "@modelcontextprotocol/server-filesystem", "/home/user/projects" ], "env": { "NODE_ENV": "production" } }, "github": { "command": "node", "args": ["mcp-github-server.js"], "env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" } }, "database": { "command": "python", "args": ["mcp-db-server.py"], "env": { "DATABASE_URL": "${DATABASE_URL}" } } } }
⚠️Warning

MCP servers have full access to the environment variables they are configured with. Never hardcode secrets in opencode.json: always use environment variable interpolation (${VAR_NAME}). The env block is passed directly to the spawned process, and any tool running in that process can read these values.


Connecting External APIs via MCP

MCP servers wrap external APIs into tool interfaces that LLMs can call:

json
{ "mcpServers": { "slack": { "command": "python", "args": ["mcp-slack-server.py"], "env": { "SLACK_BOT_TOKEN": "${SLACK_BOT_TOKEN}", "SLACK_SIGNING_SECRET": "${SLACK_SIGNING_SECRET}" } } } }
python
# mcp-slack-server.py # MCP server that wraps the Slack API into callable tools import os import httpx from mcp import Server server = Server("slack") @server.tool() async def send_message(channel: str, text: str) -> str: """Send a message to a Slack channel""" async with httpx.AsyncClient() as client: resp = await client.post( f"https://slack.com/api/chat.postMessage", headers={ "Authorization": f"Bearer {os.environ['SLACK_BOT_TOKEN']}", "Content-Type": "application/json" }, json={"channel": channel, "text": text} ) data = resp.json() if not data.get("ok"): raise Exception(f"Slack API error: {data.get('error')}") return data["message"]["text"] @server.tool() async def list_channels(limit: int = 20) -> list: """List public channels in the workspace""" async with httpx.AsyncClient() as client: resp = await client.get( "https://slack.com/api/conversations.list", headers={"Authorization": f"Bearer {os.environ['SLACK_BOT_TOKEN']}"}, params={"limit": limit} ) return resp.json()["channels"] server.run()

Writing MCP Server Implementations

An MCP server exposes three primitives:

  • Tools: Callable functions the LLM can invoke
  • Resources: Read-only data the LLM can access
  • Prompts: Pre-written prompt templates

TypeScript MCP Server

typescript
// mcp-weather-server.ts import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; const server = new Server( { name: "weather-server", version: "1.0.0" }, { capabilities: { tools: {}, resources: {} } } ); // Define a tool with JSON Schema input validation server.setRequestHandler("tools/list", async () => ({ tools: [{ name: "get_forecast", description: "Get weather forecast for a location", inputSchema: { type: "object", properties: { location: { type: "string", description: "City name or coordinates" }, days: { type: "number", description: "Number of forecast days", default: 3 } }, required: ["location"] } }] })); // Handle tool execution with error handling server.setRequestHandler("tools/call", async (request) => { const { name, arguments: args } = request.params; if (name === "get_forecast") { try { const data = await fetchWeather(args.location, args.days); return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }; } catch (error) { // Return structured error to the LLM return { content: [{ type: "text", text: `Error fetching forecast: ${error.message}` }], isError: true }; } } throw new Error(`Unknown tool: ${name}`); }); // Connect using stdio transport const transport = new StdioServerTransport(); await server.connect(transport);

Python MCP Server

python
# mcp-weather-server.py # Equivalent MCP server in Python import json import sys import httpx from mcp import Server, StdioServerTransport server = Server("weather-server") @server.list_tools() async def list_tools(): return [ { "name": "get_forecast", "description": "Get weather forecast for a location", "inputSchema": { "type": "object", "properties": { "location": {"type": "string", "description": "City name"}, "days": {"type": "number", "description": "Forecast days", "default": 3} }, "required": ["location"] } } ] @server.call_tool() async def call_tool(name: str, arguments: dict): if name == "get_forecast": async with httpx.AsyncClient() as client: resp = await client.get( f"https://api.weather.gov/points/{arguments['location']}/forecast" ) data = resp.json() return {"content": [{"type": "text", "text": json.dumps(data, indent=2)}]} async def main(): transport = StdioServerTransport() await server.connect(transport) if __name__ == "__main__": import asyncio asyncio.run(main())
📌Important

Tool schemas define the contract between the LLM and your server. Always include clear description fields for each parameter: the LLM uses these descriptions to determine how to populate arguments. A poorly described parameter will result in the LLM passing incorrect values.


Plugin Architecture

MCP servers serve as the plugin system for OpenCode. Any external capability can be wrapped as an MCP server.

Comparison: Transport Mechanisms

Aspect
stdio (subprocess)
HTTP/SSE (remote)
Process
Spawned by OpenCode
Runs independently
Latency
Low (local IPC)
Higher (network I/O)
Security
Process isolation, local only
Requires network auth, TLS
Deployment
Bundled with project
Running service or container
Lifecycle
Tied to OpenCode session
Independent daemon
Use case
Local tools (filesystem, git)
Remote APIs (Slack, GitHub, DB)
Debugging
Check server logs
Check endpoints + network
Scalability
One per session
Multiple clients
Component
Role
OpenCode
MCP client: initiates requests
MCP Server
Plugin: processes requests and returns
Transport
stdin/stdout or HTTP/SSE
Protocol
JSON-RPC 2.0
💡Tip

Use stdio transport for local development tools that need low latency (filesystem access, code analysis). Use HTTP/SSE for shared services that multiple team members need to access (shared databases, team APIs). HTTP servers can be deployed in Docker containers for consistent environments.


Tool Permission Scopes

Each MCP tool can have permission scopes defined in the configuration:

json
{ "permissions": [ { "mcpServer": "filesystem", "tools": ["read", "write"], "allow": ["/home/user/projects/*"], "deny": ["/etc/**", "/home/user/.ssh/**"] }, { "mcpServer": "github", "tools": ["create_pr", "list_repos"], "allow": ["*"], "requireApproval": true } ] }
bash
# Test MCP server connectivity from command line echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | \ node mcp-weather-server.js # Expected output: JSON-RPC response with tool definitions # You can pipe this to jq for formatted viewing: # ... | jq '.result.tools[].name'
⚠️Warning

When configuring permissions for an MCP server, remember that the server runs as a separate process. Even if OpenCode's permission system blocks a tool call, the server process itself is still running. For sensitive servers, implement authentication within the server as a defense-in-depth measure.


Practice Questions

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

An MCP server needs to communicate with the OpenCode client. What wire protocol do they use?

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

A developer is building an MCP server for a weather API. What three primitives must the server expose to the OpenCode client?

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

A team needs to connect their PostgreSQL database to OpenCode using an MCP server. The server script is mcp-db-server.py and uses DATABASE_URL. How should this be configured?

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

What is the key difference between running an MCP server via stdin/stdout versus HTTP/SSE?

Practice Question
How to answer

Multiple-choice question with 4 options. Select one option, then choose Check Answer.

An MCP server tool has a parameter without a description field in its inputSchema. What is the likely consequence?


✅Key Takeaways
  • MCP is an open standard using JSON-RPC 2.0 for LLM-to-tool communication
  • MCP servers expose tools (callable), resources (readable data), and prompts (templates)
  • Servers are configured in opencode.json under mcpServers with command, args, and env
  • External APIs (Slack, GitHub, databases) are wrapped as MCP server tools for LLM access
  • MCP servers can use stdio or HTTP/SSE as transport mechanisms
  • Permission scopes control which tools and paths each MCP server can access
  • Environment variable interpolation (${VAR_NAME}) prevents secret leakage in config files
  • The JSON-RPC protocol exchange follows a structured lifecycle: initialize, discover, execute, shutdown
  • Tool schemas must have well-described parameters for the LLM to use them correctly
  • MCP servers can be implemented in any language (TypeScript, Python, Go, etc.)
Progress80%