Skip to content

Lab 8.2: Author A Custom MCP Server

Goal

Create a tiny custom MCP server with one tool and one resource, wire it into OpenCode, and call it from an agent.

This is not a production integration lab. The goal is to understand the shape of a custom server and the design decisions behind it.

What You Will Practice

  • Designing a small MCP interface
  • Creating one domain-specific tool
  • Creating one resource the agent can read
  • Adding your local server to OpenCode config
  • Testing the server with a bounded prompt

Prerequisites

You need:

  • OpenCode installed
  • A working local development environment for either TypeScript/Node or Python
  • A folder where you can create a small server
  • Basic comfort running a local command

If the course sample MCP server template exists in your copy, start there:

labs/sample-mcp-server-template

If it does not exist yet, create a small local project using the official MCP SDK pattern for your language. Follow current MCP SDK docs for exact package names and boilerplate.

The Tiny Server Requirement

Your server must expose exactly two learner-facing capabilities:

  1. One tool that does a small action.
  2. One resource that provides context for that action.

Keep it boring. Boring is good here.

Step 1: Choose A Tiny Domain

Pick a domain you understand.

Good options:

  • Release notes
  • Commit message formatting
  • Support ticket triage
  • Feature flag naming
  • Course lesson title cleanup
  • Bug report severity labels

Avoid:

  • Payment actions
  • Production database writes
  • Sending emails or Slack messages
  • Anything that requires secrets
  • Anything that mutates real customer data

Step 2: Design The Interface

Fill this out before writing code:

Server name:
Domain:
Tool name:
Tool input:
Tool output:
Resource name:
Resource content:
Safety boundary:

Example:

Server name: release-helper
Domain: release notes
Tool name: format_release_note
Tool input: change_type, summary
Tool output: one Markdown bullet
Resource name: release-note-style-guide
Resource content: Use present tense. Keep bullets under 120 characters. Prefix with Added, Fixed, Changed, or Removed.
Safety boundary: no network, no file reads, no file writes

Step 3: Implement The Resource

Your resource can be static text.

Example content:

Release note style guide:
- Use present tense.
- Start with Added, Fixed, Changed, or Removed.
- Mention user-visible behavior.
- Do not mention internal refactors unless users benefit.
- Keep each bullet under 120 characters.

The resource gives the agent context without requiring the user to paste rules every time.

Step 4: Implement The Tool

Your tool should accept a small input and return a deterministic result.

Example behavior:

Input:
change_type = "fixed"
summary = "todo filters now show the correct items"

Output:
- Fixed todo filters so Active and Completed views show the correct items.

Keep the tool simple. It should not need network access, credentials, or file writes.

Step 5: Add The Server To OpenCode Config

Wire your local MCP server as a local command.

The config shape should look like this:

{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "release-helper": {
      "type": "local",
      "command": ["<command-that-starts-your-server>"],
      "enabled": true,
      "timeout": 30000
    }
  }
}

Examples of command shapes:

["node", "server.js"]
["python", "server.py"]

Use the command that matches your implementation.

Step 6: Restart OpenCode

Restart OpenCode so it loads the local MCP server.

If the server does not appear or fails to start, check:

  • The command works from your terminal
  • Dependencies are installed for the server project
  • The file path in config is correct
  • The server uses stdio/local transport as expected
  • The server exits only when OpenCode closes it

Step 7: Test The Resource

Ask the agent to read or use the resource only.

Example:

Use the release-helper MCP resource for release note style. Do not call the formatting tool yet. Summarize the style guide in three bullets.

You are checking whether the agent can see the resource.

Step 8: Test The Tool

Ask the agent to call the tool with simple input.

Example:

Use the release-helper MCP tool to format this release note:

change_type: fixed
summary: todo filters now show the correct items

Return only the formatted bullet.

Expected result:

- Fixed todo filters so Active and Completed views show the correct items.

Your exact output may differ, but it should follow your style resource.

Step 9: Decide Whether MCP Was Necessary

Answer honestly:

  1. Could this have been a skill instead?
  2. Could this have been a command instead?
  3. What did MCP add?
  4. What complexity did MCP add?

For many tiny examples, a skill or command would be enough. That is fine. The purpose of this lab is to learn when MCP is worth the extra moving parts.

Deliverable

Submit a lab note:

# Lab 8.2 Notes

## Design

Server name:
Domain:
Tool name:
Tool input:
Tool output:
Resource name:
Resource content:
Safety boundary:

## Config Snippet

<your OpenCode mcp config without secrets>

## Resource Test

Prompt:
Result:

## Tool Test

Prompt:
Result:

## Reflection

Could this have been a skill or command instead?
When would MCP be worth it for this domain?

Pass Criteria

You pass this lab if:

  • Your server exposes one tool and one resource.
  • The tool has clear inputs and output.
  • The resource gives useful context.
  • OpenCode can start the server through MCP config, or you document the startup error clearly.
  • You test both the resource and the tool.
  • You explain whether MCP was actually necessary.

Common Mistakes

  • Building a server that is too large for a first attempt.
  • Adding network calls before the local version works.
  • Hiding important behavior in the agent prompt instead of the server interface.
  • Forgetting to restart OpenCode after config changes.
  • Treating a custom MCP server as automatically safer than an existing one.

Stretch Challenge

Add basic validation to the tool input.

For example, reject unknown release note categories and return a helpful error:

Unknown change_type "patched". Use one of: added, fixed, changed, removed.

Then test how the agent responds when the tool rejects invalid input.