ScreenshotNeo

BlogAI agents

How to Develop an MCP Server for Web Development

Build a version-aware MCP server with TypeScript or Python, choose the right transport, test locally, and expose safe web development actions.

By the ScreenshotNeo team29 September 202610 min read

How to Develop an MCP Server for Web Development

Direct answer: develop an MCP server by choosing one narrowly scoped web action, registering it as a tool (or exposing data as a resource or reusable instruction as a prompt), validating its inputs with the SDK schema system, and serving it over stdio for a local MCP host or Streamable HTTP for a remote deployment. The current TypeScript SDK v2 uses Node.js 20 or later, an ES module project, @modelcontextprotocol/server, and Zod-compatible schemas. The Python SDK v2 requires Python 3.10 or later and provides FastMCP. Test the server with MCP Inspector before connecting it to a full host.

This guide follows the current SDK lines. The TypeScript v2 package implements the 2026-07-28 MCP specification and replaces the older monolithic @modelcontextprotocol/sdk package. The Python examples use the v2 documentation and its FastMCP API. Check the official TypeScript v2 documentation and Python SDK repository when you publish, because package APIs and requirements can change.

1. Decide what your web server should expose

Start at the application boundary, not at the protocol. Write down the single operation an AI host should perform. Examples include checking a deployment status, reading a component catalog, creating a preview build, or looking up an incident. Keep the first operation narrow enough that its inputs, side effects, permissions, and output are obvious.

An MCP server validates a model request, invokes an application action, and returns a structured result.
An MCP server validates a model request, invokes an application action, and returns a structured result.
MCP primitive Use it for Web development example
Tool An action the model invokes Trigger a preview build or query a deployment
Resource Data the client reads by URI Expose deploy://project/123/logs
Prompt A reusable prompt template Generate a release checklist from a deployment

The host chooses when to present or call these primitives, so names and descriptions matter. Describe side effects accurately, require only the arguments the operation needs, and return structured results that a model can summarize without guessing.

2. Choose TypeScript or Python and keep one version line

Use TypeScript when the application already runs in Node.js or your team wants static types shared with a web stack. The v2 TypeScript tutorial requires Node.js 20 or later and an ES module project. Use Python when your web tooling, automation, or data services are Python based; the v2 Python SDK requires Python 3.10 or later.

Do not copy imports from a v1 tutorial into a v2 project. TypeScript v1 examples commonly import from the monolithic package, while v2 separates packages such as @modelcontextprotocol/server and @modelcontextprotocol/client. The v1 line remains a maintenance line, but new v2 examples should follow the v2 package names and APIs.

3. Build a local TypeScript server over stdio

Stdio is the right transport when an MCP host launches your server as a child process. JSON-RPC messages travel over stdin and stdout. Keep stdout exclusively for protocol traffic; write diagnostics to stderr. A stray console.log can corrupt the stream and make the host report a protocol error.

Create the project

mkdir web-dev-mcp
cd web-dev-mcp
npm init -y
npm install @modelcontextprotocol/server zod
npm install --save-dev typescript tsx @types/node

Set the project to ES modules and add scripts:

npm pkg set type=module
npm pkg set scripts.start='tsx src/server.ts'
mkdir src

Create src/server.ts:

import { McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import { z } from 'zod';

const server = new McpServer({
  name: 'web-development-tools',
  version: '1.0.0'
});

server.registerTool(
  'check_deployment',
  {
    title: 'Check deployment',
    description: 'Return the current status for a deployment ID.',
    inputSchema: {
      deploymentId: z.string().min(1).max(100)
    }
  },
  async ({ deploymentId }) => {
    // Replace this bounded example with your authenticated API call.
    const result = {
      deploymentId,
      status: 'ready',
      checkedAt: new Date().toISOString()
    };

    return {
      content: [{ type: 'text', text: JSON.stringify(result) }],
      structuredContent: result
    };
  }
);

console.error('MCP server starting over stdio');
await serveStdio(server);

The declared schema is used to validate calls before the handler runs. The handler still needs application-level checks: verify that the deployment belongs to the authenticated user, enforce rate limits, and avoid returning secrets or unbounded logs.

Run and inspect it

npm start

For interactive inspection, launch MCP Inspector with the same command:

npx @modelcontextprotocol/inspector npm start

Open the Inspector interface, connect to the stdio server, list its tools, and invoke check_deployment with a valid deployment ID. Inspector is a documented workflow for exercising a server locally; it is not a replacement for authorization and integration tests.

4. The Python FastMCP equivalent

Install the Python v2 SDK with its CLI extras:

python -m venv .venv
source .venv/bin/activate
pip install 'mcp[cli]'

Create server.py:

from datetime import datetime, timezone
from mcp.server.fastmcp import FastMCP

mcp = FastMCP('web-development-tools')

@mcp.tool()
def check_deployment(deployment_id: str) -> dict:
    """Return the current status for a deployment ID."""
    if not deployment_id or len(deployment_id) > 100:
        raise ValueError('deployment_id must contain 1 to 100 characters')

    return {
        'deploymentId': deployment_id,
        'status': 'ready',
        'checkedAt': datetime.now(timezone.utc).isoformat()
    }

if __name__ == '__main__':
    mcp.run()

Run it locally with the documented development command:

mcp dev server.py

The Python SDK also documents direct in-memory client testing. That approach lets a test create a client and call a tool without spawning a subprocess or opening a port, which is useful for validating schemas and handler behavior quickly.

5. Select the transport for your deployment

Transport Best fit Operational detail
Stdio A local host that launches your process stdin/stdout carry JSON-RPC; process lifetime follows the host
Streamable HTTP A remotely hosted MCP server Use the current framework guide for sessions, authentication, and deployment
HTTP+SSE Compatibility with older clients Retained for backwards compatibility in the TypeScript documentation

For a remote TypeScript service, the SDK documentation recommends Streamable HTTP. The exact server adapter depends on your runtime and framework. The current server guide shows the general sequence: create an McpServer, register primitives, create a transport, then connect the server to that transport. Follow the server guide and selected framework guide instead of adapting a v1 transport snippet without checking imports.

For local stdio, never write request logs, stack traces, startup banners, or debug output to stdout. Use console.error in Node.js or stderr logging in Python. This one rule prevents many apparently mysterious connection failures.

6. Add resources and prompts only when they fit

A tool is not a dumping ground for every endpoint. If the host should read a stable document by URI, expose a resource. If users repeatedly need the same multi-step instruction, expose a prompt. A resource can make read-only data discoverable without pretending that reading it is an action. A prompt can standardize a workflow while leaving the actual application operation in a tool.

Keep resource URIs deterministic and access-controlled. For prompts, make arguments explicit and avoid embedding credentials or untrusted instructions. Add one primitive at a time so Inspector output remains understandable and the host can present useful descriptions.

7. Validate inputs, outputs, and side effects

  • Use an input schema with bounds for strings, numbers, arrays, and enums.
  • Reject unknown or ambiguous identifiers before making an external request.
  • Return concise text plus structured content when the client supports it.
  • Set timeouts on every upstream HTTP call and map upstream failures to actionable tool errors.
  • Make destructive operations explicit in the tool name and description.
  • Do not expose API keys, cookies, internal hostnames, raw authorization headers, or unrestricted filesystem access.

Validation in the SDK protects the handler from malformed arguments, but it does not establish identity or permission. Your application still needs authentication, authorization, audit logging, and upstream request limits, especially when the server is reachable over a network.

8. Test the server before connecting a full host

  1. Run the server directly and confirm it starts without writing protocol-breaking output.
  2. Connect with MCP Inspector and list tools, resources, and prompts.
  3. Call each tool with a valid input and confirm the result shape.
  4. Call it with missing, oversized, and wrong-type arguments to verify schema rejection.
  5. Simulate upstream timeouts and permission failures and check that messages are useful.
  6. Connect the same command to your target MCP host only after the local flow is clear.

For Python, use the documented in-memory client for fast programmatic checks. For TypeScript, keep handler logic in small functions so it can be unit tested independently from the transport, then use Inspector for protocol-level inspection.

9. Troubleshooting common errors

Symptom Likely cause Fix
Client says invalid JSON or protocol error Logs were written to stdout Replace console.log with console.error; send all diagnostics to stderr.
Tool call is rejected before the handler Arguments do not match the declared schema Inspect the schema in Inspector and send the required types, bounds, and property names.
Module not found for the server package v1 import copied into a v2 project, or dependencies not installed Install @modelcontextprotocol/server for v2 and update imports; do not mix package lines.
Server exits immediately The process reached the end, or the transport was not awaited Use await serveStdio(server) or the matching long-lived HTTP setup.
Inspector cannot start the command Wrong working directory, script name, or runtime path Run the command manually first, then pass the exact working directory and command to Inspector.
Remote requests fail while local stdio works Transport, proxy, origin, or authentication configuration is incomplete Follow the current Streamable HTTP/framework guide and inspect proxy and server logs.
Calls hang on an external API No upstream timeout or a request waiting on an unavailable service Set a finite timeout, return a clear error, and avoid retrying non-idempotent actions blindly.
Local HTTP server behaves differently by hostname Host-header or DNS-rebinding exposure Use the SDK’s host validation support and bind only as broadly as your deployment requires.

10. Performance, reliability, and cost considerations

MCP adds a protocol boundary; it does not make a slow upstream service faster. Keep tools small, use bounded responses, and avoid returning entire build logs when a summary plus a resource URI will do. Cache read-only data where its freshness allows it. For expensive operations, return a job identifier and expose a separate status tool rather than holding a connection open indefinitely.

Stdio usually has low setup overhead for a local process, but every host launch creates a process and its own connection state. Remote Streamable HTTP lets several clients reach a deployed service, but it introduces network latency, authentication, session management, proxy limits, and monitoring requirements. Measure your own upstream latency and failure rate; the SDK documentation does not provide universal performance benchmarks.

Control cost at the application boundary. Enforce per-user quotas, cap pagination, reject unbounded date ranges, and log duration and outcome without logging secrets. Retries should be limited and safe for the operation. For remote deployments, plan for graceful shutdown, health monitoring, and dependency failure so a temporary API outage becomes a clear tool error rather than a stalled model turn.

11. Or skip the browser setup

If the web-development task is taking screenshots for visual checks, documentation, or an AI workflow, ScreenshotNeo provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools. You can still build your own MCP server around your application, then let the ScreenshotNeo tools handle page capture.

A capture service can remove obstructive page overlays before returning the screenshot.
A capture service can remove obstructive page overlays before returning the screenshot.

One direct request returns a PNG, JPEG, WebP, or PDF:

curl -G 'https://api.screenshotneo.com/v1/shot' -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python:

import requests
r = requests.get('https://api.screenshotneo.com/v1/shot', params={'access_key': 'YOUR_API_KEY', 'url': 'https://stripe.com'}, timeout=90)
open('shot.webp', 'wb').write(r.content)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

See the ScreenshotNeo API documentation for the complete option list. It supports full-page capture with lazy images loaded, CSS-selector element capture, dark mode, 12 device presets or custom viewports, retina scale, PDF paper size and page ranges, HTML/CSS to image, custom CSS and JavaScript, pre-capture clicks, hidden selectors, selector/delay/network-idle waits, request and resource blocking, headers, cookies, user agents, Authorization, timezone, geolocation, transparent backgrounds, resizing, configurable caching, signed image links, async jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification. Common screenshot API parameter names also work when switching.

Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and whether it was billed. The MCP server lets an AI agent take screenshots directly. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.

12. FAQ

Can one MCP server expose both tools and resources?

Yes. Register each primitive that matches the behavior you need. Keep names, descriptions, and permissions clear so the host can distinguish actions from read-only data.

Should a browser-based MCP server use stdio?

Use stdio when a local host launches the process. A remotely hosted browser or web-development service should use the current Streamable HTTP guidance for its runtime and framework.

Is MCP authentication provided automatically?

No. The SDK handles protocol mechanics, while your deployment must define authentication, authorization, secret storage, quotas, and network exposure.

How do I migrate an old TypeScript example?

Identify whether it uses the v1 monolithic package. Then follow the v2 migration and server documentation, update package names and imports, and verify transport APIs before changing application logic.

What is the smallest useful first server?

One validated, read-only tool with a bounded response, a timeout around its upstream call, stderr diagnostics, and an Inspector workflow is a good starting point. Add resources, prompts, and write operations only when a real host workflow needs them.