Back to Blog
June 16, 2026

Windsurf Cascade and MCP: How Tool-Use Limits Your Agentic Workflows

Your agent is only as good as the tools it can choose, so design the menu before you blame the model.

Windsurf Cascade MCP workflow dashboard with curated tools

Windsurf Cascade plus MCP feels like the indie hacker dream: connect GitHub, Linear, Postgres, Stripe, docs, logs, a browser, and your deployment platform, and suddenly your coding agent can do the boring half of a launch day. The trap is that agentic workflow quality does not scale with the number of buttons you add. It scales with how clearly the agent can choose the next right action.

The official Cascade MCP docs say Cascade can connect to MCP servers and use custom tools like GitHub, databases, and APIs. They also describe a practical constraint that changes everything: Cascade has a limit of 100 total tools available at any given time. That is not a footnote. That is your architecture budget.

This matters because AI coding tools are no longer a fringe experiment. The Stack Overflow 2025 Developer Survey reports that 84% of respondents are using or planning to use AI tools in development, and 51% of professional developers use AI tools daily. GitHub Octoverse 2025 reports more than 1.1 million public repositories using an LLM SDK, with 693,867 created in the prior 12 months. The vibe is real. The tooling surface is exploding. Your job is to keep it shippable.

Why does Cascade's MCP tool limit change your workflow?

Because every exposed tool competes for attention, context, trust, and routing. Tool count is not capacity; it is cognitive load.

MCP is a standard way for AI applications to connect to external context and capabilities. The protocol describes hosts, clients, and servers using JSON-RPC 2.0, with servers exposing resources, prompts, and tools. In Cascade, Windsurf is the host experience, Cascade acts as the MCP client, and your local or remote server exposes the things the agent can call.

The limit is healthy. Without it, most teams would dump every API operation into the agent and call that automation. That usually creates worse workflows: duplicate tools, unclear names, raw endpoints with missing validation, and long tool descriptions that burn context before the agent writes a line of code. A smaller menu forces product thinking. What job should the agent complete? What inputs are safe? What output helps the next step?

A good Cascade MCP setup is less like installing plugins and more like designing a command palette for a junior engineer who moves incredibly fast. Make the good path obvious, make the dangerous path explicit, and make failure easy to inspect.

The practical model: jobs beat endpoints

The most common MCP mistake is mapping one API endpoint to one tool. That feels natural if you think like an SDK author. It is usually wrong if you think like an agent designer. Agents do not need your API surface. They need durable jobs with sharp boundaries.

For an indie SaaS, a raw Stripe server might expose dozens of operations. Your agent rarely needs all of them. It needs things like summarizeRecentFailedPayments, createCustomerBillingLink, or inspectSubscriptionHealth. Each should do validation, pagination, retries, rate-limit handling, and output shaping inside the tool. The agent should receive the decision-ready result, not 600 lines of raw JSON.

Design choiceRaw endpoint MCPWorkflow MCP
Tool shapelistIssues, getIssue, updateIssuetriageBugReport, inspectReleaseFailure
Agent burdenPlans API choreography every timeChooses an outcome-oriented action
Failure modeSilent partial state and vague errorsTyped, contextual, retryable failure

This is also why QA thinking helps vibecoders. A tool is a contract. The agent is your caller. If the contract is vague, you get flaky automation. If the contract is crisp, your workflow compounds. For a related angle on shipping with automation checks, read our event-driven testing deep dive.

Code example 1: audit your MCP tool budget before Cascade does

This script checks generated MCP tool manifests before you wire them into Cascade. It catches duplicate tool names, missing descriptions, empty servers, and budget overflows before the editor gets involved.

#!/usr/bin/env node
import { readFile } from 'node:fs/promises';

const MAX_TOOLS = Number(process.env.MAX_MCP_TOOLS ?? 100);

async function loadManifest(path) {
  try {
    const raw = await readFile(path, 'utf8');
    const parsed = JSON.parse(raw);
    if (!parsed.serverName || typeof parsed.serverName !== 'string') {
      throw new Error('serverName must be a non-empty string');
    }
    if (!Array.isArray(parsed.tools)) throw new Error('tools must be an array');
    return parsed;
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    throw new Error('Failed to load ' + path + ': ' + message);
  }
}

const files = process.argv.slice(2);
if (files.length === 0) {
  console.error('Pass at least one manifest file.');
  process.exit(2);
}

const manifests = await Promise.all(files.map(loadManifest));
const seen = new Map();
const problems = [];
let total = 0;

for (const manifest of manifests) {
  if (manifest.tools.length === 0) {
    problems.push(manifest.serverName + ' exposes zero tools. Did auth fail during discovery?');
  }

  for (const tool of manifest.tools) {
    total += 1;
    const name = String(tool.name ?? '').trim();
    const description = String(tool.description ?? '').trim();
    if (!name) problems.push(manifest.serverName + ' has a tool without a name');
    if (description.length < 30) problems.push(manifest.serverName + '/' + name + ' needs a better description');

    const owner = seen.get(name);
    if (owner) problems.push('Duplicate tool name ' + name + ' in ' + owner + ' and ' + manifest.serverName);
    else seen.set(name, manifest.serverName);
  }
}

if (total > MAX_TOOLS) problems.push('Tool budget exceeded: ' + total + '/' + MAX_TOOLS);

if (problems.length > 0) {
  console.error('MCP audit failed:\n- ' + problems.join('\n- '));
  process.exit(1);
}

console.log('MCP audit passed: ' + total + '/' + MAX_TOOLS + ' tools across ' + manifests.length + ' servers.');

Why this works: it treats tool discovery as build metadata, not as something you eyeball in the editor. Duplicate names matter because tool names are part of the model's routing surface. If three servers expose search or query with vague descriptions, you made the model's job harder for no payoff.

Can one MCP tool replace ten smaller tools?

Yes, when the ten calls are one business job. No, when bundling hides permissions, side effects, or decisions the user must approve.

The rule is simple: merge mechanics, separate decisions. Fetching issue details, commits, failed checks, and deploy logs can be one inspectReleaseFailure tool. Merging a PR, charging a card, deleting data, or sending an email should remain explicit and permissioned. The agent should not smuggle side effects inside a friendly helper.

Code example 2: generate a curated Cascade MCP config

Cascade lets users toggle tools on each MCP settings page. For repeatable solo workflows, put that idea in version control. This script reads a ranked policy and writes a config with disabledTools for everything outside your active budget.

#!/usr/bin/env node
import { readFile } from 'node:fs/promises';

const policyPath = process.argv[2];
if (!policyPath) {
  console.error('Usage: node curate-mcp-config.mjs policy.json');
  process.exit(2);
}

let policy;
try {
  policy = JSON.parse(await readFile(policyPath, 'utf8'));
} catch (error) {
  console.error('Could not read policy: ' + (error instanceof Error ? error.message : String(error)));
  process.exit(1);
}

const maxTools = Number(policy.maxTools ?? 100);
const selected = [];
const disabledByServer = new Map();

for (const server of policy.servers ?? []) {
  if (!server.name || !server.command) throw new Error('Each server needs name and command');
  const tools = [...(server.tools ?? [])].sort((a, b) => Number(b.priority ?? 0) - Number(a.priority ?? 0));
  disabledByServer.set(server.name, []);

  for (const tool of tools) {
    if (!tool.name) throw new Error(server.name + ' has a tool without name');
    const risky = tool.sideEffect === 'destructive' && tool.requiresApproval !== true;
    const tooMany = selected.length >= maxTools;

    if (risky || tooMany || tool.enabled === false) disabledByServer.get(server.name).push(tool.name);
    else selected.push(server.name + '/' + tool.name);
  }
}

if (selected.length === 0) {
  console.error('Refusing to write config with zero enabled tools.');
  process.exit(1);
}

const mcpServers = Object.fromEntries((policy.servers ?? []).map((server) => [
  server.name,
  {
    command: server.command,
    args: server.args ?? [],
    env: server.env ?? {},
    disabledTools: disabledByServer.get(server.name),
  },
]));

console.error('Enabled ' + selected.length + '/' + maxTools + ' tools');
console.log(JSON.stringify({ mcpServers }, null, 2));

Give high priority to tools that support your weekly shipping loop: inspect failing tests, summarize user feedback, query recent exceptions, create a preview environment, and prepare a release note. Push raw admin tools down unless you truly use them inside Cascade.

Code example 3: wrap risky API calls in one safe workflow tool

This JSON-RPC-style handler turns a noisy deployment API into one workflow tool: inspectPreviewDeploy. It handles missing env vars, bad input, timeouts, non-JSON responses, and the edge case where a branch exists but has no deploy yet.

const input = JSON.parse(process.argv[2] ?? '{}');
const token = process.env.DEPLOY_API_TOKEN;

function fail(code, message, details = {}) {
  return { ok: false, code, message, details };
}

function validateBranch(branch) {
  if (typeof branch !== 'string' || branch.trim().length === 0) return fail('bad_input', 'branch is required');
  if (branch.includes('..') || branch.startsWith('/')) {
    return fail('bad_input', 'branch contains unsafe path-like characters', { branch });
  }
  return { ok: true, branch: branch.trim() };
}

async function fetchJson(url, options) {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 8000);
  try {
    const response = await fetch(url, { ...options, signal: controller.signal });
    const text = await response.text();
    let body;
    try {
      body = text ? JSON.parse(text) : null;
    } catch {
      return fail('bad_response', 'Deploy API returned non-JSON', { status: response.status, sample: text.slice(0, 120) });
    }
    if (!response.ok) return fail('api_error', 'Deploy API request failed', { status: response.status, body });
    return { ok: true, body };
  } catch (error) {
    const aborted = error instanceof Error && error.name === 'AbortError';
    return fail(aborted ? 'timeout' : 'network_error', aborted ? 'Deploy API timed out' : String(error));
  } finally {
    clearTimeout(timeout);
  }
}

async function inspectPreviewDeploy(args) {
  if (!token) return fail('missing_secret', 'DEPLOY_API_TOKEN is not set');
  const valid = validateBranch(args.branch);
  if (!valid.ok) return valid;

  const url = 'https://deploy.example.com/api/previews?branch=' + encodeURIComponent(valid.branch);
  const result = await fetchJson(url, { headers: { Authorization: 'Bearer ' + token } });
  if (!result.ok) return result;

  const deploy = result.body?.deploys?.[0];
  if (!deploy) return fail('not_found', 'No preview deploy exists for this branch yet', { nextAction: 'trigger_preview_deploy' });

  return {
    ok: true,
    branch: valid.branch,
    status: deploy.status,
    url: deploy.url,
    commitSha: deploy.commitSha,
    nextAction: deploy.status === 'failed' ? 'fetch_deploy_logs' : 'open_preview_and_run_smoke_tests',
  };
}

console.log(JSON.stringify(await inspectPreviewDeploy(input), null, 2));

The important design choice is the result shape. The tool returns ok, code, message, details, and nextAction. That gives Cascade enough structure to recover instead of guessing the next command. For more workflow design patterns, see our guide to agentic QA workflows.

Troubleshooting: when Cascade MCP feels flaky

Most MCP pain is boring configuration pain wearing an AI costume. Debug it like infrastructure before you blame the model.

  • Tool is missing in Cascade: check whether MCP is enabled for your plan or team, confirm the server appears in Cascade settings, then count active tools.
  • Server starts locally but not in Cascade: run the exact command from mcp_config.json in a clean shell. Your terminal may have env vars the editor process lacks.
  • OAuth or token failures: verify the transport type and callback URL. Return missing_secret with the env var name instead of a generic 500.
  • Agent picks the wrong tool: rename tools around outcomes, add examples to descriptions, and delete duplicates. Bad routing is often a UX bug in your tool catalog.
  • Long context or noisy output: summarize inside the tool. Return top findings, stable IDs, and next actions instead of raw logs.

The edge cases that bite side projects

Side projects have weird constraints. You do not have a platform team. You probably have one production database, one preview environment, and one Stripe account you really do not want an agent to mutate accidentally. Design for that reality.

  • Shared personal tokens: if the MCP server runs with your token, the agent has your permissions. Prefer scoped tokens and read-only defaults.
  • Pagination traps: a tool that only reads the first page will confidently miss the failing item. Handle cursors inside the tool.
  • Eventual consistency: deploys, payments, and webhooks may lag. Return pending states and recommended retry windows instead of pretending not_found is final.
  • Prompt injection through external data: issues, docs, and logs can contain hostile instructions. Treat tool output as data, not instructions to the agent.

The shipping heuristic: expose fewer tools than you think you need, then add tools only when you see a repeated workflow. If a tool is used once a month, it can probably stay in docs or a CLI.

The bottom line

Windsurf Cascade and MCP can make a solo builder feel like a tiny product team. But the leverage comes from disciplined tool design, not from installing the largest catalog. Treat the tool limit as a forcing function: name tools by outcomes, merge mechanical API steps, split risky decisions, validate inputs, shape outputs, and test the catalog in CI.

The best agentic workflows feel boring when they work. The agent chooses the obvious tool, gets compact context, handles the edge case, and moves to the next step. That is the vibe worth chasing: not magic, not chaos, just a fast feedback loop that lets you ship the side project without turning your editor into a slot machine.

Ready to ship your next project faster?

Desplega.ai helps indie hackers and solopreneurs build and ship faster with AI-assisted QA, automation checks, and practical release workflows.

Get Started

Frequently Asked Questions

Does Windsurf Cascade support MCP?

Yes. Cascade supports MCP servers and can use tools, resources, and prompts, but you still need to curate tools because the active tool budget is limited.

Why do MCP tool limits matter for indie hackers?

Tool limits force prioritization. A focused workflow with ten reliable tools beats a giant catalog that confuses routing, hides failures, and slows shipping.

Should every API endpoint become an MCP tool?

No. Wrap real jobs, not raw endpoints. A good MCP tool should validate inputs, handle retries, return compact context, and expose one useful outcome.

How do I debug a Cascade MCP server that works locally?

Start with config path, env vars, transport type, logs, and tool count. Then test the server outside Cascade with the same command and environment.