Security
13 min read
24 views

Local-First AI Agent Testing: How to Expose Local MCP Servers Safely Without Compromising Host Security

Learn how to expose local Model Context Protocol (MCP) servers safely using instatunnel. Secure local AI agent testing, streaming responses, and remote debugging.

IT
InstaTunnel Team
Published by the InstaTunnel team | Editorial policy
Local-First AI Agent Testing: How to Expose Local MCP Servers Safely Without Compromising Host Security

Quick answer

Secure Local MCP Servers with Instatunnel: AI Agent Testing: webhook testing answer

For local webhook testing, run your app locally, expose it with a public HTTPS tunnel, and paste the stable callback URL into the provider dashboard.

How do I test webhooks on localhost?

Start your local server, open a public HTTPS tunnel to that port, configure the provider webhook URL, and inspect events in your local logs.

Why does a stable webhook URL matter?

Stable URLs prevent provider dashboards from needing manual callback updates every time you restart a tunnel.

Introduction: Local Tools, Remote Clients

AI engineering has moved a lot of work onto developer machines. Agents call local tools, read local files, and query local databases through the Model Context Protocol (MCP). Anthropic introduced MCP, and in December 2025 donated it to the Agentic AI Foundation, a directed fund under the Linux Foundation. It is now a vendor-neutral open standard.

The friction shows up when something that isn’t on your machine needs to reach your MCP server. That might be a hosted agent, a teammate’s client, or a staging environment. A tunnel is the fastest fix, but it turns a loopback-only process into a public HTTPS endpoint. A local MCP server is a program running with your user account’s privileges, so whatever it is coded to touch, anyone who can reach it can try to touch.

This guide covers four things:

  • how MCP’s transports work today
  • which threats a tunnel actually changes
  • how to expose a server with InstaTunnel using only features its documentation describes
  • how to layer authentication so the tunnel is not your only defense

MCP Transports in 2026

The current specification defines two standard transports: stdio and Streamable HTTP. The older HTTP+SSE transport from the 2024-11-05 revision is deprecated, and new implementations should not adopt it.

stdio: the local default

With stdio, the client launches the MCP server as a subprocess and exchanges JSON-RPC messages over standard input and output. The spec says clients SHOULD support stdio whenever possible.

  • Privileges: the server is a child process of your user account, so it can reach whatever your account can reach and the server’s code allows.
  • Scope: there is no network listener. Only the client that spawned it can talk to it, which is why security guidance for local servers favors stdio.
  • Authentication: the authorization spec says stdio implementations SHOULD NOT follow the OAuth flow and should take credentials from the environment instead.

A remote client cannot spawn a process on your laptop, so exposing a stdio server remotely needs a network layer, either a bridge or an HTTP-native server.

Streamable HTTP: the remote transport

In the latest revision (2026-07-28), Streamable HTTP is much simpler than earlier versions:

  • The server exposes a single endpoint (for example /mcp) that accepts POST.
  • Every JSON-RPC message is its own POST. The server replies with either a single JSON object or a Server-Sent Events (SSE) stream scoped to that request. That stream carries progress notifications and ends with the final response.
  • Protocol-level sessions, the initialize handshake, the standalone GET stream and Last-Event-ID resumability are all gone. Clients speaking 2025-03-26 through 2025-11-25 used the old shape, including Mcp-Session-Id.
  • POSTs carry MCP-Protocol-Version, Mcp-Method and, for calls like tools/call, Mcp-Name headers, so gateways can route without parsing JSON bodies.
  • Closing the response stream counts as cancelling that request.

Two consequences matter for tunnels:

  1. Dropped connections are not resumable. A tunnel that drops mid-stream means the client must re-issue the request, so keep tool calls safe to retry.
  2. Intermediaries can buffer streams. The spec says servers SHOULD send X-Accel-Buffering: no when opening an SSE stream. It also encourages periodic SSE comment lines as keep-alives on long-lived streams so intermediaries don’t close idle connections. Remote agent / test client │ HTTPS POST /mcp (JSON or SSE response) ▼ InstaTunnel public edge (https://<subdomain>.instatunnel.my) │ tunnel ▼ Local MCP server on 127.0.0.1:8787

What the Specification Requires of an HTTP Server

For Streamable HTTP the spec’s security section says:

  1. Servers MUST validate the Origin header on all incoming connections to prevent DNS rebinding. An invalid Origin gets HTTP 403.
  2. Servers running locally SHOULD bind only to localhost (127.0.0.1), not 0.0.0.0.
  3. Servers SHOULD implement proper authentication for all connections.

Authorization itself is optional in MCP. When an HTTP server does implement it, the spec builds on OAuth 2.1 and says the server acts as a resource server. Among its requirements:

  • Servers MUST validate that access tokens were issued for them as the intended audience.
  • Servers MUST NOT accept or pass along other tokens.
  • Invalid or expired tokens MUST receive a 401.
  • Access tokens MUST NOT go in the URI query string.

The Threat Model When You Add a Tunnel

A tunnel does not create new MCP vulnerabilities. It changes who can reach the ones you already have.

Prompt injection reaching tool arguments. Models build tool arguments from context, and context can contain attacker text. A Cloud Security Alliance research note describes a demonstrated chain in which a crafted Slack message, handled through an already-approved Slack MCP server, steered Cursor into rewriting its global mcp.json to add an attacker-controlled server. A tunnel does not cause this, but it widens the set of clients that can call your tools. Remote clients are also less likely to have a human approving each call.

Tool poisoning. Malicious instructions can sit in tool descriptions that the model sees but the user usually doesn’t. Invariant Labs published the original advisory in April 2025. Review tool definitions, pin versions, and treat descriptions as part of your supply chain.

Unauthenticated endpoints and DNS rebinding. CVE-2025-49596 was an unauthenticated remote code execution flaw in MCP Inspector. Its proxy exposed an endpoint with no authentication between client and proxy. It was fixed in 0.14.1. Oligo also demonstrated the same RCE through DNS rebinding, where an attacker’s web page reaches a localhost service. Current Inspector builds require a proxy token by default, bind to localhost, and validate Origin. Binding to loopback alone was not enough before the fix.

Token passthrough and confused deputy. The MCP security best practices describe token passthrough as an anti-pattern. A server accepts tokens that weren’t issued to it and forwards them to a downstream API. The authorization spec explicitly forbids it, because it can bypass controls such as rate limiting that depend on token audience. If your local server proxies to another API, validate the audience and never forward the client’s token.

Reference servers are demonstrations. The official modelcontextprotocol/servers repository carries a warning that its servers are reference implementations that demonstrate MCP features and SDK usage. The Filesystem and Git servers are still maintained there, while the SQLite server has moved to an archived repository that offers no security guarantees. Don’t put one behind a public URL just because it is official.

What InstaTunnel Documents for MCP

The details below come from the npm README for the instatunnel CLI (version 1.1.23). Check the current docs before relying on them, since the CLI changes quickly.

Installing and signing in

npm install -g instatunnel

# New users: start the verification-email flow
instatunnel auth login -e you@example.com

# Existing users: save an API key
instatunnel auth set-key "it_your_api_key"

MCP mode

  • MCP tunnels are listed as a Pro and Business feature, described as beta on the README’s feature list.
  • instatunnel 8787 --mcp starts an MCP tunnel. Appending /mcp to the generated URL gives the endpoint your client calls.
  • --transport v1 is the default. The README recommends --transport v2 for streaming, meaning MCP Streamable HTTP, long-lived tool responses and server-push-like flows. v2 needs CLI 1.1.7 or newer.
  • instatunnel doctor --mcp runs MCP readiness checks.
  • The README lists “MCP security controls: staged token header + stricter MCP rate limits”, plus “dedicated protective limits” for MCP traffic.

The staged token header

The README documents an optional header, X-InstaTunnel-MCP-Token, configured through an environment variable:

export INSTATUNNEL_MCP_TOKEN="your-shared-secret"
instatunnel 8787 --mcp --transport v2

The README does not say whether the edge enforces this header, strips it, or passes it through to your server. It also doesn’t say whether remote clients must send it themselves. Treat it as an outer gate and test its behavior in your own setup. Your server should enforce its own authentication and not depend on that header arriving.

Other documented controls

  • --password (password protect) and --auth user:pass (basic auth) appear in the flag reference. Browser-style prompts may not suit a non-interactive MCP client, so test before relying on them.
  • --subdomain sets a custom subdomain on instatunnel.my.
  • --logs polls request logs (Pro/Business), --list shows active tunnels, and --kill <subdomain> stops one.

What the documentation does not claim

The README describes HTTPS URLs. It does not describe end-to-end encryption to your machine, and it doesn’t describe header rewriting beyond the staged token. Don’t assume either.

Streaming Through the Tunnel

MCP tool calls can run long and emit progress, so buffering is the main failure mode.

  • Use transport v2 when a client expects streaming responses, per the InstaTunnel README.
  • Send X-Accel-Buffering: no on SSE responses. This is a spec SHOULD, so check what your server actually sends: curl -N -i -X POST https://<subdomain>.instatunnel.my/mcp \ -H "Authorization: Bearer $MCP_TOKEN" \ -H "Content-Type: application/json" \ -H "Accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Look at the response headers and confirm that events arrive as they are produced.

  • Emit keep-alive comments (a line beginning with a colon) on long-lived streams if idle timeouts bite.
  • Make retries safe, because a stream that dies mid-call cannot be resumed in the current revision.
  • Don’t invent protocol details. Hand-rolled handlers that write ad hoc data: events, or an OpenAI-style [DONE] sentinel, are not MCP. Use the official SDK so framing, headers and error codes match the spec.

Layered Authentication for Tunneled MCP

Relying on an obscure URL is not a security control. If you pick a memorable subdomain like mymcp, anyone can guess it. Use layers so a failure in one doesn’t expose your tools:

  1. Edge gate (optional): InstaTunnel’s staged token header, with the caveats above.
  2. Server authentication (required): a bearer token the server checks on every request. InstaTunnel’s own README suggests configuring the MCP server to require Authorization: Bearer <token>. A shared secret is acceptable for a short-lived dev session. For anything longer-lived or multi-user, follow the spec’s OAuth 2.1 resource-server model with audience validation.
  3. Host and Origin validation (required): reject unexpected Host and Origin values.
  4. Capability limits (required): expose only the tools the test needs, with strict input schemas, no shell execution, and read-only file scopes.

Hands-On: A Locked-Down MCP Server Behind InstaTunnel

This uses the official TypeScript SDK (v2), the stable line implementing the 2026-07-28 spec. The v1 @modelcontextprotocol/sdk package stays supported for at least six months after v2’s release but follows different APIs.

Step 1: Create the project

mkdir local-mcp-secure && cd local-mcp-secure
npm init -y
npm pkg set type=module
npm install @modelcontextprotocol/server @modelcontextprotocol/express @modelcontextprotocol/node express zod
npm install -D tsx typescript @types/express @types/node

Step 2: Write the server

Create server.ts:

import { timingSafeEqual } from 'node:crypto';
import type { NextFunction, Request, Response } from 'express';
import { createMcpExpressApp } from '@modelcontextprotocol/express';
import { toNodeHandler } from '@modelcontextprotocol/node';
import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';

const PORT = 8787;
const TOKEN = process.env.MCP_TOKEN ?? '';
const PUBLIC_HOST = process.env.MCP_PUBLIC_HOST; // e.g. acme-mcp-dev.instatunnel.my

if (TOKEN.length < 32) {
  console.error('Set MCP_TOKEN to a random value of at least 32 characters.');
  process.exit(1);
}

function tokenMatches(presented: string): boolean {
  const a = Buffer.from(presented);
  const b = Buffer.from(TOKEN);
  return a.length === b.length && timingSafeEqual(a, b);
}

function requireToken(req: Request, res: Response, next: NextFunction) {
  const [scheme, presented] = (req.headers.authorization ?? '').split(' ');
  if (scheme !== 'Bearer' || !presented || !tokenMatches(presented)) {
    res.set('WWW-Authenticate', 'Bearer').status(401).json({ error: 'unauthorized' });
    return;
  }
  next();
}

// A fresh McpServer is built for every request.
const handler = createMcpHandler(() => {
  const server = new McpServer({ name: 'local-notes', version: '1.0.0' });
  server.registerTool(
    'add-note',
    {
      description: 'Echo a short note (demo tool, stores nothing)',
      inputSchema: z.object({ text: z.string().max(500) })
    },
    async ({ text }) => ({ content: [{ type: 'text', text: `Received: ${text}` }] })
  );
  return server;
});

// Host/Origin validation: allow loopback names plus the tunnel's public hostname.
const allowedHosts = ['127.0.0.1', 'localhost', ...(PUBLIC_HOST ? [PUBLIC_HOST] : [])];
const app = createMcpExpressApp({ host: '0.0.0.0', allowedHosts });

const node = toNodeHandler(handler);
app.all('/mcp', requireToken, (req, res) => void node(req, res, req.body));

// Bind loopback only; the tunnel agent connects from the same machine.
app.listen(PORT, '127.0.0.1', () => {
  console.log(`MCP server on http://127.0.0.1:${PORT}/mcp`);
});

Why it is written this way:

  • createMcpExpressApp applies express.json() and Host/Origin validation. The SDK docs show the host: '0.0.0.0' plus allowedHosts form for serving a non-localhost hostname, and requests without an Origin header (non-browser MCP clients) pass through. The actual socket is bound separately by app.listen, and binding it to 127.0.0.1 follows the spec’s SHOULD for local servers.
  • The auth middleware sits in front of the MCP handler, following the SDK’s own app.all('/mcp', auth, ...) pattern. It uses a constant-time comparison and returns 401 with a WWW-Authenticate header, as the spec requires for invalid tokens.
  • The tool schema is strict and the tool performs no side effects. Add real tools one at a time.

Step 3: Generate secrets and start the server

export MCP_TOKEN="$(node -e "console.log(require('crypto').randomBytes(32).toString('hex'))")"
export MCP_PUBLIC_HOST="acme-mcp-dev.instatunnel.my"
npx tsx server.ts

Step 4: Open the tunnel

In a second terminal:

export INSTATUNNEL_MCP_TOKEN="$(node -e "console.log(require('crypto').randomBytes(32).toString('hex'))")"
instatunnel 8787 --mcp --transport v2 --subdomain acme-mcp-dev
instatunnel doctor --mcp

Use a separate secret for the staged header and for the bearer token, so one leak doesn’t reveal both. If the server answers 403 on every tunneled request, compare the Host header the tunnel forwards with your allowedHosts list.

Step 5: Test from the remote side

Check that an unauthenticated request is refused, whether by the edge or by your server’s 401:

curl -i -X POST https://acme-mcp-dev.instatunnel.my/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Then check that an authenticated request works (add the staged header if your setup requires it):

curl -i -X POST https://acme-mcp-dev.instatunnel.my/mcp \
  -H "Authorization: Bearer $MCP_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

The SDK’s own example shows a tools/list reply arriving as a single SSE message event. Real MCP clients built on the SDK also send the MCP-Protocol-Version and Mcp-Method headers on their own.

Step 6: Point an MCP client at it

The InstaTunnel README gives this shape for IDE or client settings. Key names vary by client, so check yours:

{
  "mcpServers": {
    "instatunnel-remote": {
      "type": "streamable-http",
      "url": "https://acme-mcp-dev.instatunnel.my/mcp",
      "headers": {
        "Authorization": "Bearer YOUR_MCP_TOKEN"
      }
    }
  }
}

Never put the token in the URL. The spec forbids access tokens in query strings, and URLs leak into logs and history.

Step 7: Tear it down

Stop the tunnel with Ctrl+C or instatunnel --kill acme-mcp-dev, then stop the server. Rotate both secrets before the next session.

Hardening Checklist

  • Bind to loopback and validate Host and Origin, as the spec requires.
  • Require authentication on every request. For anything beyond a short dev session, use OAuth 2.1 with audience validation.
  • Never forward a client’s token to a downstream API.
  • Rate-limit at the entry point, in front of or alongside the token check, to slow brute-force guessing. InstaTunnel’s README says MCP traffic gets stricter limits at the edge, but don’t treat that as your only throttle.
  • Minimize tools. Disable write, delete and shell-execution tools unless the test needs them, and constrain inputs with schemas. Review tool descriptions as you would code.
  • Run the server unprivileged, ideally in a container or a dedicated user account with only the files the test needs.
  • Keep a human in the loop for sensitive tool calls where the client supports it.
  • Watch the traffic. Use instatunnel --logs (Pro/Business) and your own server logs.
  • Keep tunnels short-lived, and rotate tokens after each session.
  • Keep dependencies current. MCP tooling has had serious CVEs, including the Inspector issue above.

Conclusion

Testing agents against local MCP servers is practical, as long as you treat the tunnel as publishing an endpoint with your privileges behind it. Use the official SDK so the transport and headers match the spec. Bind to loopback, validate Host and Origin, and require a bearer token in your own server. Add InstaTunnel’s staged token header as an extra outer gate, but verify how it behaves rather than assuming. Expose the smallest set of tools that does the job, and close the tunnel when the test is over.

Sources

Continue from this article into the most relevant product guides and workflows.

Related Topics

#Model Context Protocol#MCP server security#local AI agent testing#instatunnel#expose local server#secure local LLM#remote debugging AI agents#Model Context Protocol workflows#streaming responses local AI#staged token headers#host security AI testing#local-first AI development#MCP server remote debugging#secure localhost tunneling#AI agent framework security#Model Context Protocol tutorial#instatunnel use cases#local LLM tool servers#secure API tunneling#local AI infrastructure#MCP server configuration#LLM tool execution security#tunneling for AI developers#secure tunneling local AI#MCP server authentication#AI agent debugging tools#localhost exposure security#model context protocol implementation#local LLM integration security#zero trust tunneling AI#secure proxy for local LLM#MCP server authorization#local AI workflow security#testing AI agents locally#debug local MCP servers#expose localhost securely#instatunnel configuration#tunneling local LLM servers#AI agent development security#Model Context Protocol guide#secure webhook testing local#local-first software architecture#LLM agent debugging#secure remote access local AI#proxying streaming responses#server-sent events security#local tool server security#AI agent testing best practices#developer tools for AI#secure tunnel for LLM#local AI debugging guide#MCP server proxy#secure local development tunnel#AI agent infrastructure security

Keep building with InstaTunnel

Read the docs for implementation details or compare plans before you ship.

Share this article

More InstaTunnel Insights

Discover more tutorials, tips, and updates to help you build better with localhost tunneling.

Browse All Articles