I wanted to call MCP tools from automation scripts without burning tokens on an AI call or dealing with non-deterministic results. Tools like BrowserMCP are great for browser automation, but I didn't want an LLM deciding what to click — I wanted my script in control. The official MCP client SDK lets you connect to any MCP server from a plain JavaScript script and call its tools programmatically — no AI agent needed. Here is how to do it.
Connecting to an MCP Server
Most MCP servers run as child processes and communicate over stdio. The SDK provides StdioClientTransport to spawn and talk to these processes. You create a transport pointing to the server's entry file, instantiate a Client, and connect them:
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
async function connectToMCPServer(command, args) {
const transport = new StdioClientTransport({ command, args })
const client = new Client({
name: 'my-script',
version: '1.0.0'
})
await client.connect(transport, { timeout: 120000 })
return client
}
The command should be the Node.js binary path, and args should include the path to the MCP server's entry file. For example, to connect to a custom MCP server:
const client = await connectToMCPServer(
'/usr/local/bin/node',
['/path/to/mcp-server/dist/index.js']
)
Some MCP servers are distributed as npm packages or standalone binaries. For instance, BrowserMCP connects to a browser. You'd point the transport to its entry file:
const transport = new StdioClientTransport({
command: '/usr/local/bin/node',
args: ['./node_modules/@example/mcp-server/cli.js']
})
Once connected, the client gives you two main methods: listTools() to discover available tools and callTool() to execute them.
// Discover what tools the server offers
const tools = await client.listTools()
console.log('Available tools:', tools.tools.map(t => t.name))
// Call a tool with arguments
const result = await client.callTool({
name: 'fetch_weather',
arguments: { city: 'London', units: 'celsius' }
})
MCP tools return their results as an array of content blocks. The most common type is text, which contains a JSON string. You parse it with a regex to extract the structured data:
if (result.isError) {
console.error('Tool failed:', result)
return
}
const textContent = result.content[0].text
const jsonMatch = textContent.match(/\[.*\]/s)
if (jsonMatch) {
const data = JSON.parse(jsonMatch[0])
console.log('Result:', data)
}
The isError flag lets you detect tool execution failures without relying on exceptions. Always check it before processing the response.
Cleanup is important — MCP servers run as child processes and won't exit on their own. Use a try/finally block to ensure the client is always closed:
let client
try {
client = await connectToMCPServer(nodePath, serverArgs)
// ... use the client
} finally {
if (client) {
await client.close()
}
}
I use this pattern extensively in automation projects. For mobile device automation, I connect to @mobilenext/mobile-mcp to list connected Android devices, launch apps, tap at specific coordinates, and type text into search fields. For browser automation, I connect to @railsblueprint/blueprint-mcp to create browser tabs, navigate to URLs, and interact with page elements. In both cases, there is no AI agent — the scripts call client.callTool() directly, controlling the tool based on logic and user input.
MCP is a versatile protocol that goes beyond AI assistants. Any CLI tool or service can be wrapped as an MCP server and consumed from any JavaScript script using the pattern above. If you want to build your own server to expose custom tools, check out my guide on building a custom MCP server. You can also learn more about Claude Code best practices for AI-assisted workflows that complement these scripts.