Integrating MCP (Model Context Protocol) with OpenCode for Web Dev

Jul 23, 2026 min read

You ask your local agent to write an Express route that aggregates monthly sign-ups, and it hands back a query filtering on users.created_date. Your column is created_at. The agent didn’t get this wrong out of carelessness — it never saw your database. It guessed at a plausible schema because a plausible schema was the only thing available to it. Every previous article in this series has been about narrowing the gap between what the model assumes and what’s actually true. This time, the gap isn’t in the code it writes. It’s in the data it’s writing code against.

That’s the ceiling every local model hits eventually. Qwen3.5 9B can be flawless at Express routing, Zod validation, and Tailwind layout, but none of that helps when the actual blocker is zero visibility into your live PostgreSQL instance. Paste the schema into the prompt by hand and it works — until the schema drifts, nobody updates the prompt, and you’re back to hallucinated columns.

The Model Context Protocol (MCP) solves this by giving the agent a standing connection to the thing it’s guessing about instead of a static description of it. Anthropic introduced it, and by mid-2026 it’s the mechanism Cursor, Windsurf, and OpenCode all use the same way. Think of it as “USB-C for AI” — one standard connector instead of a different bespoke integration for every tool. Point OpenCode at your database through an MCP server, and Qwen3.5 9B can fetch the schema itself, on demand, and write the aggregation route against what’s actually there. Still fully offline. Still nothing leaving your machine.

Understanding Model Context Protocol (MCP)

The Bridge to the Real World

MCP runs on a client-server split that maps cleanly onto pieces you already have: OpenCode is the client, and an MCP server sits between it and whatever external resource you’re exposing — a database, a filesystem, an API. It’s built on JSON-RPC, which in practice means the model doesn’t receive a static data dump. Instead, it receives a set of tools it can call, like query or list_tables, or resources (file-like paths) representing the schema. The agent decides when to reach for one, the same way it decides when to open a file.

That’s the real shift. Without MCP, giving your agent database awareness means copy-pasting a schema into the prompt and hoping it stays current. With MCP attached, the agent fetches the live schema itself at the exact moment it needs it. The code it generates is checked against the current table structure, not a paste from three migrations ago.

Model Context Protocol (MCP) Diagram

This diagram visualizes the architecture of the Model Context Protocol (MCP) in a local environment. It illustrates how the OpenCode client (powered by Qwen3.5 9B) interacts with an external database using the MCP Server as an intermediary to safely fetch schema information.

Model Context Protocol (MCP) Diagram

(QweOnp3e.n5Co9dBeAgent)ToJoSlONR-eRsPuClts(MLCoPcaSlerNvoedre)DataS/QSLchemaP(orsetagdroenSlQyL_rDoBle)

Visual Notes:

  • The MCP Server acts as a secure bridge, translating JSON-RPC tool calls from the agent into standard SQL queries.
  • Read-only database credentials ensure that even if the AI generates a destructive query, it is blocked at the database level.

Why Local MCP Is Powerful

Running MCP alongside a local model extends the privacy-first setup this series has been building toward. The MCP server runs on your machine, talking to a database on your machine, and Qwen3.5 9B never sends a single row over the network. You get the capability of a schema-aware agent without shipping anything to a cloud API.

The security boundary that makes this safe to actually use is permissions, not obscurity. Reference database MCP servers expose read-oriented tools by default. The community convention is to restrict destructive operations like DROP or DELETE at the credential level, not just hope the model behaves. That distinction matters: don’t rely on the model choosing not to run a dangerous query. Make it structurally unable to do so.

Setting Up an MCP Server

Connecting to a Database

The reference implementations — @modelcontextprotocol/server-postgres and @modelcontextprotocol/server-sqlite — run via npx, so there’s no separate service to install and babysit. Point OpenCode’s MCP config at your database:

{
  "mcpServers": {
    "local-postgres": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-postgres",
        "postgresql://readonly_user:password@localhost:5432/dbname"
      ]
    }
  }
}

Notice the username: readonly_user, not postgres. That’s not a placeholder to swap out later, that’s the actual configuration you want. Create a database role scoped to SELECT only, and use that role’s credentials here — never the superuser account. If you’re working with SQLite instead, the setup is even simpler: point the args at the local .db file path directly, which keeps the entire round trip — model, server, and data — inside your project directory.

Verifying the Connection

Once OpenCode restarts with the config loaded, ask it directly: “What MCP tools do you have available?” A working connection returns a list including query and perhaps explicit schema tools like list_tables or get_schema. Depending on the MCP server version, the schema might also be exposed as a resource instead of a tool, or the model might just use the query tool to read information_schema. If the list of tools and resources comes back empty, the connection didn’t establish — check the troubleshooting section below.

Practical Example: Generating a Complex Analytics Route

The scenario: an Express route that aggregates user sign-ups by month, without you typing out the users table schema anywhere in the prompt.

Step 1 — start OpenCode with the MCP server attached. Confirm the tool list is live using the check above.

Step 2 — prompt without describing the schema:

“Inspect the users table via your MCP tools (or by querying information_schema) and write an Express route at GET /api/analytics/signups-by-month that returns sign-up counts grouped by month, using Prisma.”

Step 3 — watch the agent fetch before it writes. A working session shows the tool call happening before any code appears — something like:

[tool_call] query("SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'users'")
[tool_result] [
  { "column_name": "id", "data_type": "uuid" },
  { "column_name": "email", "data_type": "text" },
  { "column_name": "created_at", "data_type": "timestamp without time zone" }
]

Only after that comes back does the agent write the route, now correctly referencing created_at instead of guessing at created_date. We also cast the count to an integer to prevent JSON serialization errors with Prisma’s BigInt return types:

// routes/analyticsRoutes.js
const express = require('express');
const { PrismaClient } = require('@prisma/client');

const router = express.Router();

// Best practice: import a shared Prisma instance, but shown inline here for clarity
const prisma = new PrismaClient();

// GET /api/analytics/signups-by-month
// Aggregates user sign-ups by calendar month, based on the actual `created_at`
// column confirmed via MCP schema inspection — not assumed.
router.get('/signups-by-month', async (req, res) => {
  try {
    const results = await prisma.$queryRaw`
      SELECT
        DATE_TRUNC('month', created_at) AS month,
        COUNT(*)::int AS signups
      FROM users
      GROUP BY month
      ORDER BY month ASC
    `;

    return res.status(200).json(results);
  } catch (error) {
    console.error('signups-by-month error:', error);
    return res.status(500).json({ error: 'Internal server error' });
  }
});

module.exports = router;

That’s a route generated against your actual schema, by a model that fetched the schema itself thirty seconds earlier — no manual copy-paste, and no cloud database connection outside your machine.

Best Practices and Tips

Do:

  • Connect the MCP server with dedicated read-only credentials. This is the actual security boundary, not a suggestion — a SELECT-only role can’t truncate a table no matter what the model decides to run.
  • Tell the model directly: “Use your MCP tools to check the schema before writing the route.” Smaller models benefit from the explicit nudge rather than relying on them to reach for tools unprompted every time.

Don’t:

  • Hand the MCP server postgres superuser credentials. If the connection string has write access, a hallucinated or misinterpreted query has write access too.
  • Let the agent run SELECT * FROM users against a table with real PII just to “look around.” That pulls sensitive data straight into the local context window for no reason — ask it to inspect the schema, not the contents.

Troubleshooting Common Issues

The agent ignores the MCP tools entirely. Tool-calling is a harder skill for a 9B model than plain code generation, and it can default to guessing at a schema instead of reaching for its tools. Add an explicit instruction to AGENTS.md: “You MUST inspect the schema via MCP tools before writing any route or query that touches the database. Do not assume column names.” Naming the tools directly closes most of this gap.

Connection refused or authentication failures. Usually one of two things: the npx process can’t reach a database that isn’t actually running on localhost:5432, or the connection string is mangling a special character in the password. URL-encode anything unusual in the credentials (@, #, %) before it goes in the connection string, and confirm the database is up with a plain psql connection first.

A query blows up the context window. An exploratory query call that returns 10,000 rows doesn’t just look ugly in the terminal — it eats a meaningful chunk of the model’s effective attention budget on data it doesn’t need in full. Instruct the model to always append LIMIT 10 on exploratory queries, and reserve unbounded queries for the final generated route, not the model’s own investigation step.

Conclusion

MCP turns OpenCode from a capable code generator into a system architect that actually knows the system it’s architecting for. Even a 9B model handles tool calls reliably once you tell it explicitly to use them, and the payoff is real: routes and queries built against your live schema instead of a guess, with nothing leaving your machine. Every piece from this series — the frontend, the API, the AGENTS.md rules, the debugging loop — was building toward an agent that could reason about your actual infrastructure instead of an imagined version of it. This is that last piece.

Next steps: Build a small custom MCP server of your own in Node.js or Python for a data source the reference implementations don’t cover, and start planning the deployment of the full application this series has walked through.

Related Articles in This Series:

Sources