ScreenshotNeo

BlogAI agents

MCP Server in JavaScript: Build, Run, and Deploy One

Build an MCP server in JavaScript with the current TypeScript SDK, validate tools, run over stdio, and choose remote HTTP when needed.

By the ScreenshotNeo team1 October 20267 min read

Short answer: create a Node.js project, install the current MCP v2 server package, define a tool with a Zod schema, attach a transport, and let an MCP host launch or connect to the process. The server exposes capabilities; the host and model provide the user interface and reasoning.

This guide targets the stable v2 TypeScript SDK line, which uses @modelcontextprotocol/server and implements the documented MCP specification revision 2026-07-28. The older v1 package, @modelcontextprotocol/sdk, has different imports and migration requirements. Check the official MCP documentation before publishing because package APIs and protocol revisions can change.

1. Understand the MCP server model

An MCP client or host connects to your server, discovers its capabilities, and calls them when appropriate.

Capability Purpose Typical example
Tools Actions that can validate input and perform work or side effects Search an issue tracker or create a report
Resources Reference data a client can read Project documentation or a configuration file
Prompts Reusable message templates A standard code-review prompt

A minimal server can expose one tool. Add resources or prompts only when the client needs them. The model does not run inside the server, and the server does not provide the host UI. Hosts can include Claude Code, VS Code, Cursor, or a custom application; follow the target host’s current connection instructions.

2. Create a Node.js project

The official first-server walkthrough uses Node.js 20 or later, npm, ES modules, Zod, and tsx so TypeScript can run without a separate build step.

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

Edit package.json:

{
  "name": "mcp-weather-server",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "tsx src/server.ts"
  }
}

The type field matters because the SDK is distributed as ES modules. Create the source directory:

mkdir src

3. Register a useful tool

Create src/server.ts. This example follows the official tool pattern: a descriptive name, a Zod input schema, and a handler that returns protocol content.

import { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio.js";
import { z } from "zod";

const server = new McpServer({
  name: "weather-server",
  version: "1.0.0"
});

server.registerTool(
  "get_weather_alerts",
  {
    title: "Get weather alerts",
    description: "Return active weather alerts for a US state.",
    inputSchema: {
      state: z.string().length(2).describe("Two-letter US state code")
    }
  },
  async ({ state }) => {
    const code = state.toUpperCase();

    // Replace this with your real API call.
    const message = `No active alerts found for ${code}.`;

    return {
      content: [{ type: "text", text: message }]
    };
  }
);

const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Weather MCP server running over stdio");

Schema validation runs before the handler. Invalid input, such as a three-letter state code, is rejected without executing your business logic. Return protocol content from the handler; do not print result data directly to stdout.

4. Run locally over stdio

Stdio is the usual choice when a local MCP host launches your server as a child process. Start it directly while developing:

npm start

Keep stdout reserved for MCP protocol messages. Send diagnostics to stderr with console.error. A stray console.log can corrupt the protocol stream and make the host report an apparently unrelated parse error.

A host configuration generally needs the command and arguments that start the process. The exact JSON shape varies by host, so use that host’s current documentation. A typical command is:

npx tsx /absolute/path/mcp-weather-server/src/server.ts

5. Inspect and invoke the tool

The official Inspector provides a local web interface for connecting to a command, discovering capabilities, supplying arguments, and viewing the result. Run it with your server command:

npx @modelcontextprotocol/inspector npx tsx src/server.ts

Open the local address printed by Inspector, connect, select get_weather_alerts, enter a two-letter state code, and inspect the returned content. This is a documentation workflow; verify the current Inspector command and UI in the MCP documentation.

6. Add resources and prompts when needed

Resources are for data a client reads. They should not hide expensive computation or unexpected side effects. Tools are the better fit for actions. Prompts package reusable messages for the client.

The exact v2 registration signatures are version-sensitive, so consult the SDK API reference before copying these concepts into production. Keep each capability narrow, name it clearly, and document input and output formats.

7. Choose a transport

Transport Use it when Operational implications
stdio A local host owns and launches the process No listening port; process lifetime follows the host
Streamable HTTP Clients need a remote endpoint Deploy and secure an HTTP service; verify host support
HTTP+SSE An older client requires compatibility The v1 guidance marks it deprecated for new implementations

For a remote deployment, add authentication, TLS, request limits, structured logging, and isolation appropriate to the data and actions your tools expose. Confirm the target host’s supported transport before choosing an adapter. Do not assume a local stdio configuration can be pointed at a public URL without host-specific changes.

8. Make tools reliable

  • Validate every argument with Zod, including length, format, ranges, and allowed values.
  • Set timeouts on upstream HTTP calls and return a useful protocol error when they expire.
  • Keep handlers deterministic where possible; make write operations explicit and idempotent when practical.
  • Never place secrets in tool descriptions, logs, prompts, or returned content.
  • Return concise results and include identifiers or links the host can display.
  • Use stderr for logs in stdio mode and redact tokens, cookies, and personal data.
  • For HTTP, authenticate before dispatching a tool and apply per-user authorization inside the handler.

9. Performance and cost considerations

MCP itself does not provide a published performance or cost benchmark. Your latency is usually dominated by the upstream systems a tool calls, serialization, and process startup.

  • Reuse clients and connections instead of creating a new SDK client on every call.
  • Cache read-only data with an explicit freshness policy.
  • Limit result size; paginate large datasets and let the client request the next page.
  • Run long work asynchronously when the chosen host and transport support it.
  • For stdio, avoid repeated process startup if your host can keep a server alive.
  • For HTTP, measure p50 and p95 latency, error rates, upstream timeouts, and concurrent sessions.

10. Common errors and fixes

Symptom Likely cause Fix
Package or export not found v1 and v2 packages or imports were mixed Use @modelcontextprotocol/server consistently for v2, or follow the v1 migration guide for an existing project.
Host reports invalid JSON or protocol data Debug output was written to stdout Move logs to console.error and keep stdout exclusively for MCP traffic.
Tool call is rejected before the handler Arguments do not match the Zod schema Inspect the generated schema and send correctly typed, bounded values.
Server exits immediately The transport was never connected or an uncaught startup error occurred Await server.connect(transport), run the command manually, and inspect stderr.
Inspector cannot connect Wrong command, path, runtime, or transport Use an absolute path, confirm Node.js 20+, and run the same command outside Inspector.
Remote client cannot reach the server Unsupported transport, firewall, TLS, or authentication failure Verify Streamable HTTP support, endpoint reachability, certificates, and credentials separately.
Requests hang Upstream call has no timeout or a promise is never resolved Add an abortable timeout, log lifecycle events to stderr, and return a bounded error.

11. SDK version checklist

  • Use the v2 package name and imports together.
  • Confirm the documented protocol revision before release.
  • Use Node.js 20 or later for the official walkthrough.
  • Read the migration guide before upgrading a v1 project.
  • Verify transport support in the MCP host you plan to use.

12. Or skip the browser setup

If your MCP tool only needs website images or PDFs, ScreenshotNeo provides a ready-made screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. Cookie and consent banners, newsletter popups, and chat widgets are removed before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and each response reports its verdict and billing status in X-Page-Verdict and X-Billed headers.

Install no browser automation stack for the basic call:

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

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
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}`);

See the ScreenshotNeo API documentation for the complete option set: full-page and element capture, device presets, retina scale, dark mode, custom CSS and JavaScript, waits, request blocking, headers and cookies, geolocation, PDF output, resizing, caching, signed links, async webhooks, bulk capture, and usage reporting. Its MCP server includes take_screenshot, get_page_info, and capture_pdf for AI clients.

There is a free tier of 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots, and every feature is available on every plan. Create a free ScreenshotNeo account.

FAQ

Does an MCP server contain the language model?

No. It exposes tools, resources, and prompts to a client or host. The host connects the model and user interface.

Can I write an MCP server in plain JavaScript?

Yes, when the runtime and package setup support JavaScript. The official walkthrough uses TypeScript with tsx because it provides typed schemas and runs directly during development.

Should every server use HTTP?

No. Use stdio for a local process launched by a host. Use Streamable HTTP when clients need a remote service.

Can a resource perform an action?

Resources are intended for data access. Put side effects and operations in tools so the client can present them as callable actions.

How do I upgrade a v1 server?

Do not replace imports mechanically. Read the SDK migration guidance, update the package and transport setup together, then reconnect the server through Inspector and the target host.