🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@mcpspend/proxy

Package Overview
Dependencies
Maintainers
1
Versions
14
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@mcpspend/proxy - npm Package Compare versions

Comparing version
0.3.0
to
0.3.1
+1
dist/proxy.test.d.ts
export {};
"use strict";
// Tests for extractServerName. The function is not exported from proxy.ts so
// we re-import via require and access the internal binding. If the API ever
// grows we'll bring it out into its own module.
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const node_test_1 = require("node:test");
const strict_1 = __importDefault(require("node:assert/strict"));
// Internal binding access — proxy.ts doesn't export extractServerName because
// it's an implementation detail. We dynamic-require to expose it for tests
// without changing the public surface.
// eslint-disable-next-line @typescript-eslint/no-require-imports
const proxy = require('./proxy.js');
// If the module doesn't expose a test hook, re-implement the same dispatcher
// inline. We'd rather duplicate three lines than couple production code to
// the test layout.
function extractName(command, args) {
if (proxy.__testExtractServerName)
return proxy.__testExtractServerName(command, args);
throw new Error('proxy.ts must expose __testExtractServerName for tests — see proxy.ts diff in this PR');
}
(0, node_test_1.describe)('extractServerName', () => {
(0, node_test_1.test)('playwright via @playwright/mcp@latest', () => {
strict_1.default.equal(extractName('npx', ['-y', '@playwright/mcp@latest']), 'playwright');
});
(0, node_test_1.test)('filesystem via @modelcontextprotocol/server-filesystem', () => {
strict_1.default.equal(extractName('npx', ['-y', '@modelcontextprotocol/server-filesystem', '/data']), 'filesystem');
});
(0, node_test_1.test)('github via @modelcontextprotocol/server-github', () => {
strict_1.default.equal(extractName('npx', ['-y', '@modelcontextprotocol/server-github']), 'github');
});
(0, node_test_1.test)('fetch via mcp-server-fetch (uvx)', () => {
strict_1.default.equal(extractName('uvx', ['mcp-server-fetch']), 'fetch');
});
(0, node_test_1.test)('firecrawl via firecrawl-mcp', () => {
strict_1.default.equal(extractName('npx', ['-y', 'firecrawl-mcp']), 'firecrawl');
});
(0, node_test_1.test)('github via github-mcp-server', () => {
strict_1.default.equal(extractName('npx', ['-y', 'github-mcp-server']), 'github');
});
(0, node_test_1.test)('node script with mcp-server suffix', () => {
strict_1.default.equal(extractName('node', ['./my-mcp-server.js']), 'my');
});
(0, node_test_1.test)('Windows absolute path node script', () => {
strict_1.default.equal(extractName('node', ['C:\\Users\\me\\servers\\notion-mcp.js']), 'notion');
});
(0, node_test_1.test)('skips npm shims, finds package after', () => {
strict_1.default.equal(extractName('npx', ['--yes', '@some-org/brave-search']), 'brave-search');
});
(0, node_test_1.test)('falls back to last token when nothing matches conventions', () => {
const out = extractName('python', ['./custom_runner.py']);
strict_1.default.ok(out.length > 0, 'should produce something');
strict_1.default.notEqual(out, 'python');
});
});
+1
-1

@@ -7,3 +7,3 @@ #!/usr/bin/env node

const init_js_1 = require("./init.js");
const VERSION = '0.3.0';
const VERSION = '0.3.1';
const HELP = `mcpspend — observability proxy for MCP servers (v${VERSION})

@@ -10,0 +10,0 @@

import type { Config } from './config.js';
export declare const __testExtractServerName: (command: string, args: string[]) => string;
export declare function runProxy(opts: {

@@ -3,0 +4,0 @@ command: string;

@@ -6,2 +6,3 @@ "use strict";

Object.defineProperty(exports, "__esModule", { value: true });
exports.__testExtractServerName = void 0;
exports.runProxy = runProxy;

@@ -18,25 +19,76 @@ const cross_spawn_1 = __importDefault(require("cross-spawn"));

}
// Strip an npm package spec down to its identifying core. We do this in stages
// so each rule is auditable and reviewers can extend it without unwinding a
// monster regex.
//
// @playwright/mcp@latest → playwright
// @modelcontextprotocol/server-fs → fs
// @owner/foo-mcp → foo
// github-mcp-server → github
// mcp-server-fetch → fetch
// firecrawl-mcp → firecrawl
function stripMcpAffixes(s) {
let t = s;
t = t.replace(/^mcp-server-/i, '').replace(/-mcp-server$/i, '');
t = t.replace(/^server-/i, '').replace(/-server$/i, '');
t = t.replace(/^mcp-/i, '').replace(/-mcp$/i, '');
return t.toLowerCase().trim();
}
function isDegenerate(t) {
return !t || t === 'mcp' || t === 'server' || t === 'latest';
}
function normaliseServerToken(raw) {
let t = raw;
// Drop everything after the version separator, but only when it's a version,
// not a scope marker. `@playwright/mcp@latest` → `@playwright/mcp`.
const lastAt = t.lastIndexOf('@');
if (lastAt > 0)
t = t.slice(0, lastAt);
// If this is a scoped npm spec like `@playwright/mcp`, try the unscoped
// name first; if that strips down to something generic ("mcp", "server"),
// fall back to the scope itself. That gives "playwright" instead of "mcp"
// for `@playwright/mcp@latest`, while still preferring the specific name
// for `@modelcontextprotocol/server-filesystem` → "filesystem".
let scope = null;
if (t.startsWith('@')) {
const slash = t.indexOf('/');
if (slash > 0) {
scope = t.slice(1, slash).toLowerCase();
t = t.slice(slash + 1);
}
}
// Path basename when this is a script path.
t = t.split(/[\\/]/).pop() || t;
t = t.replace(/\.(js|cjs|mjs|ts|tsx|py)$/i, '');
let name = stripMcpAffixes(t);
if (isDegenerate(name) && scope) {
name = stripMcpAffixes(scope);
}
if (isDegenerate(name))
return null;
return name;
}
// Exported only for tests — see proxy.test.ts. Kept off the public surface to
// avoid implying it's a stable API.
const __testExtractServerName = (command, args) => extractServerName(command, args);
exports.__testExtractServerName = __testExtractServerName;
function extractServerName(command, args) {
// Best-effort: pick the most descriptive token from the command line.
// Examples:
// npx @modelcontextprotocol/server-filesystem /path → "filesystem"
// node ./my-mcp-server.js → "my-mcp-server"
// /usr/bin/uvx mcp-server-fetch → "mcp-server-fetch"
const tokens = [command, ...args];
// Skip well-known shims that never carry the server identity themselves.
const skipPrefix = new Set(['npx', 'npx.cmd', 'npx.exe', 'uvx', 'pnpx', 'bunx', 'pipx', 'node', 'bun', 'deno', 'python', 'python3']);
const tokens = [command, ...args].filter((t) => {
if (!t)
return false;
if (t.startsWith('-'))
return false; // flags
const base = t.split(/[\\/]/).pop()?.toLowerCase() || '';
return !skipPrefix.has(base) && !skipPrefix.has(t.toLowerCase());
});
for (const t of tokens) {
const m = t.match(/(?:server-|mcp-server-)([a-z0-9-]+)/i);
if (m)
return m[1];
const name = normaliseServerToken(t);
if (name)
return name;
}
for (const t of tokens) {
const m = t.match(/([a-z0-9-]+)-mcp-server/i);
if (m)
return m[1];
}
// Fallback: last non-flag arg's basename without extension
const lastArg = [...tokens].reverse().find((t) => !t.startsWith('-'));
if (lastArg) {
return lastArg.split(/[\\/]/).pop().replace(/\.[^.]+$/, '');
}
return 'mcp';
// Last resort — last raw token basename.
const lastArg = [...args].reverse().find((t) => !t.startsWith('-')) || command;
return (lastArg.split(/[\\/]/).pop() || lastArg).replace(/\.[^.]+$/, '') || 'mcp';
}

@@ -43,0 +95,0 @@ async function runProxy(opts) {

{
"name": "@mcpspend/proxy",
"version": "0.3.0",
"version": "0.3.1",
"description": "Transparent proxy CLI for MCP servers — tracks tool calls, latency, and cost via MCPSpend.",

@@ -39,3 +39,3 @@ "license": "MIT",

"dev": "tsx src/cli.ts",
"test": "node --test --import tsx src/clients.test.ts",
"test": "node --test --import tsx src/clients.test.ts src/proxy.test.ts",
"typecheck": "tsc --noEmit",

@@ -42,0 +42,0 @@ "prepublishOnly": "npm run build"