Remote MCP security setup
1. MCP enters to juvenile years
The March 2025 update to the Model Context Protocol (MCP) Specification introduced support for remote MCP servers and a new transport layer based on Streamable HTTP. With this update, developers can now host their MCP servers on platforms like AWS EC2 or DigitalOcean, making them accessible via a public URL. This eliminates the need for users to install and run MCP servers locally.
Remote MCP servers offer two major benefits: developers can keep their code and logic private, and they can reduce latency when accessing data located behind firewalls.
We expect more enterprise platforms—such as FactSet APIs—to adopt remote MCP architecture, enabling richer, AI-driven development ecosystems.
However, the majority of the 6,000+ MCP servers currently on GitHub do not yet support Streamable HTTP transport. Enabling remote access to an MCP server involves more than just updating the transport layer—it requires security configurations. Without them, your AI project could quickly turn into a security nightmare.
Most importantly, MCP server developers must now take on additional roles: acting as both system administrator and security administrator of the remote services. These responsibilities demand different expertise than coding alone. Ensuring scalability, availability, and security is a full-time job. So, don’t rush to publish your remote MCP server URL without a clear security and infrastructure plan in place.
2. Access Control: The Non-Negotiable First Step
The most critical setup for any remote MCP server is access control. Never expose your MCP server to the open internet without restrictions. Unfortunately, the authentication method recommended in the MCP specification— OAuth 2.1 IETF DRAFT —is still a work in progress. It’s complex, not widely adopted yet, and tooling support remains immature. I recommend waiting until the ecosystem around this spec stabilizes before adopting it.
So how should you securely publish remote MCP servers today? Treat them like any other web app or API endpoint. Implement API key checks on every request and reject any that are invalid or expired. This simple step is fundamental to remote service management—it lays the groundwork for rate limiting, audit logging, and even usage-based billing.
Traditional solutions like NGINX or commercial API gateways can help enforce API key control. However, they come with trade-offs: you’ll either need to maintain a 24/7 NGINX instance or provide a credit card to an API gateway vendor. And here’s the catch—if your traffic suddenly spikes or a DDoS attack hits, your API gateway bill could hit you like a train.
3. Free, 24x7 API key gateway using Google AppScript
Don't limit yourself to traditional tools like NGINX or Cloudflare. With Google Apps Script, you can create a lightweight API gateway that offers global access, 24/7 uptime, basic logging, and a clear upgrade path—all for free.
The key is to publish your Google Apps Script as a Web App Deployment. You then use UrlFetchApp.fetch()
to forward incoming requests to your backend MCP server, without exposing its IP address to users. This effectively places your remote MCP server behind Google’s infrastructure, shielding it from direct access while saving you from the burden of managing an NGINX server or facing surprise API gateway charges during high traffic or DDoS events.
Once deployed, your Web App handles incoming MCP client requests via doGet()
or doPost()
. The first step in either function should be to validate the API key in the HTTP request before forwarding it to the backend using UrlFetchApp.fetch()
.
Below is a basic implementation. Naturally, the management of API keys (e.g. adding, revoking) can be automated and moved out of hardcoded logic.
Implementation details will follow in the next post.
const BACKEND_BASE = 'http://192.0.2.10:3000'; // 2) Valid API keys const VALID_API_KEYS = [ 'YOUR_SECURE_API_KEY_1', 'YOUR_SECURE_API_KEY_2', // Add more keys as needed ]; /** * Validate the provided API key */ function isValidApiKey(key) { return VALID_API_KEYS.indexOf(key) !== -1; } /** * Proxy GET requests * Example: GET /exec?path=/foo&apikey=KEY¶m=1 */ function doGet(e) { const apiKey = e.parameter.apikey; if (!isValidApiKey(apiKey)) { return jsonError(401, 'Unauthorized: Invalid or missing API key'); } const pathParam = e.parameter.path; if (!pathParam) { return jsonError(400, 'Missing "path" parameter'); } const url = buildProxyUrl(pathParam, e.parameter); const options = getFetchOptions(e, 'GET'); const response = UrlFetchApp.fetch(url, options); return ContentService .createTextOutput(response.getContentText()) .setMimeType(ContentService.MimeType.JSON) .setHttpStatusCode(response.getResponseCode()); }