npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@noetaris/harness-mcp

v0.3.0

Published

MCP server integration for @noetaris/harness

Readme

@noetaris/harness-mcp

MCP (Model Context Protocol) client integration for @noetaris/harness. Connect your agents to MCP servers over HTTP or stdio and expose their tools directly to the harness tool-calling pipeline.

Overview

@noetaris/harness-mcp provides two main building blocks:

  • MCPClient — connects to a single MCP server (HTTP or stdio), discovers its tools, and exposes them as Tool[] compatible with the harness tool schema
  • MCPManager — manages a pool of MCPClient instances, merges their tool lists, and supports live config reload via file watching

Config files (.json, .ts, .js, .mjs) can be loaded with MCPManager.fromConfig() or watched for changes with MCPManager.watch().

Installation

pnpm add @noetaris/harness-mcp

This package does not declare @noetaris/harness-types as a peer dependency — it uses Tool only via import type, which is erased at compile time, so nothing is required at runtime. Install @noetaris/harness-types as a dev dependency only if you want the matching Tool type for your own TypeScript code:

pnpm add -D @noetaris/harness-types

Quick Start

import { MCPClient, MCPManager } from '@noetaris/harness-mcp'

// single HTTP server
const client = await MCPClient.fromHttp('http://localhost:3100/mcp')
console.log(client.tools()) // Tool[]

// multiple servers via manager
const manager = new MCPManager([client])
await manager.addServer({ command: 'npx', args: ['my-mcp-server'] })
console.log(manager.tools()) // merged Tool[] — last-write-wins on duplicate names

API Reference

MCPClient

Wraps a single MCP server connection. Use the static factory methods to create instances; the constructor is private.

MCPClient.fromHttp(url, options?): Promise<MCPClient>

Connects to an MCP server over Streamable HTTP and discovers its tools.

const client = await MCPClient.fromHttp('http://localhost:3100/mcp', {
  prefix: 'search',     // optional: prefix all tool names with "search/"
  rediscover: 'per-session', // optional: re-run tool discovery on each harness session start
})

| Option | Type | Description | |--------|------|-------------| | prefix | string | Prepended to all tool names as {prefix}/{name}. Useful to namespace tools from different servers. | | rediscover | "per-session" | Re-discovers tools at the start of each harness run (via bindObserver). Omit for static tool lists. |

MCPClient.fromStdio(params): Promise<MCPClient>

Launches a subprocess and connects to it over stdio.

const client = await MCPClient.fromStdio({
  command: 'npx',
  args: ['@acme/mcp-server', '--port', '0'],
  env: { API_KEY: process.env.API_KEY! },
  prefix: 'acme',
})

| Parameter | Type | Description | |-----------|------|-------------| | command | string | The executable to run. | | args | string[] | Arguments passed to the command. | | env | Record<string, string> | Environment variables for the subprocess. | | prefix | string | Tool name prefix (same as HTTP). | | rediscover | "per-session" | Per-session rediscovery (same as HTTP). |

client.tools(): Tool[]

Returns the cached tool list from the last discover() call.

client.discover(): Promise<void>

Re-queries the server for its tool list and updates the cache. Called automatically on construction; call manually to refresh.

client.disconnect(): Promise<void>

Closes the underlying transport. Always call this when the client is no longer needed to avoid resource leaks.


MCPManager

Manages a pool of MCPClient instances. Tool name conflicts are resolved last-write-wins (later clients in the pool overwrite earlier ones for the same tool name).

new MCPManager(clients, options?)

const manager = new MCPManager([clientA, clientB], {
  rediscover: 'per-session', // applies to all clients that don't set their own rediscover option
})

manager.tools(): Tool[]

Returns the merged tool list across all connected servers.

manager.addServer(params): Promise<void>

Connects to a new server and adds it to the pool. Accepts the same parameters as MCPClient.fromHttp (pass { url: '...' }) or MCPClient.fromStdio (pass { command: '...' }).

await manager.addServer({ url: 'http://localhost:3200/mcp', prefix: 'tools' })
await manager.addServer({ command: 'my-mcp-server', prefix: 'local' })

manager.removeServer(key): Promise<void>

Disconnects and removes a server by its URL (HTTP) or command (stdio). Throws MCPServerNotFoundError if no server matches the key.

await manager.removeServer('http://localhost:3200/mcp')
await manager.removeServer('my-mcp-server')

manager.loadConfig(path): Promise<void>

Loads server definitions from a config file and adds each as a new server. See Config File Format below.

manager.watch(path, options?): Promise<() => Promise<void>>

Watches a config file for changes and hot-reloads servers. Changes are debounced by 100ms. Returns a dispose function to stop watching.

const stopWatching = await manager.watch('./mcp.config.json', {
  onError: (err) => console.error('MCP reload error:', err),
})

// later
await stopWatching()

If a reload fails to add a new server, previously added servers from that reload batch are rolled back before onError is called.

manager.bindObserver(observer): void

Binds the manager to a harness observer. Clients configured with rediscover: 'per-session' will call discover() at the start of each harness run. Pass the observer you register with harness.addObserver().

const manager = new MCPManager([client], { rediscover: 'per-session' })
harness.addObserver(manager.bindObserver.bind(manager))

MCPManager.fromConfig(path): Promise<MCPManager>

Static factory. Creates a new manager and loads servers from a config file in one step.

const manager = await MCPManager.fromConfig('./mcp.config.json')

Config File Format

Config files can be .json, .ts, .js, or .mjs. The schema is:

interface MCPConfigSchema {
  servers: MCPServerEntry[]
}

JSON example:

{
  "servers": [
    { "url": "http://localhost:3100/mcp", "prefix": "search" },
    {
      "transport": "stdio",
      "command": "npx",
      "args": ["@acme/mcp-server"],
      "env": { "API_KEY": "secret" },
      "prefix": "acme",
      "rediscover": "per-session"
    }
  ]
}

TypeScript example (mcp.config.ts):

export default {
  servers: [
    { url: 'http://localhost:3100/mcp' },
    { transport: 'stdio' as const, command: 'my-server' },
  ],
}

HTTP entries default to transport: 'http' and require a url field. Stdio entries require transport: 'stdio' and a command field. Both support optional prefix and rediscover fields.


Error Classes

MCPServerNotFoundError

Thrown by removeServer() when no client matches the given key.

import { MCPServerNotFoundError } from '@noetaris/harness-mcp'

try {
  await manager.removeServer('http://gone.invalid')
} catch (err) {
  if (err instanceof MCPServerNotFoundError) {
    console.error(err.key) // 'http://gone.invalid'
  }
}

MCPConfigParseError

Thrown by loadConfig() and watch() when the config file is structurally invalid.

import { MCPConfigParseError } from '@noetaris/harness-mcp'

try {
  await manager.loadConfig('./bad-config.json')
} catch (err) {
  if (err instanceof MCPConfigParseError) {
    console.error(err.path)   // file path
    console.error(err.detail) // what was wrong
  }
}

MCPConfigExtensionError

Thrown when the config file has an unsupported extension.

import { MCPConfigExtensionError } from '@noetaris/harness-mcp'

License

MIT