New:Socket for Asana Is Now Available.Learn more
Sign In

transistor-mcp

Package Overview
Dependencies
Maintainers
1
Versions
3
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

transistor-mcp - npm Package Compare versions

Comparing version
0.3.0
to
0.4.0
+140
build/errors.js
/**
* Typed error hierarchy for the Transistor.fm API client.
*
* `TransistorApiClient` maps every HTTP failure to one of these classes based
* on status code (see `mapHttpStatusToError`) via a single axios response
* interceptor registered in its constructor, so callers can branch on error
* type with `instanceof` instead of parsing status codes out of a generic
* message. `ToolHandlers` catches `TransistorError` and surfaces
* `error.message` directly in the MCP `isError` response — each subclass
* builds its own fully-formatted, human-readable message so that formatting
* logic lives in one place.
*
* Ported from the same pattern in conorbronsdon/podcastindex-mcp (v0.3.0).
*/
export class TransistorError extends Error {
status;
constructor(message, status) {
super(message);
this.name = "TransistorError";
this.status = status;
Object.setPrototypeOf(this, TransistorError.prototype);
}
}
/** HTTP 401/403 — bad, missing, or expired API credentials. */
export class AuthenticationError extends TransistorError {
constructor(detail, status) {
super(`Authentication error (${status}): ${detail}. Check that TRANSISTOR_API_KEY is correct and has not been revoked in the Transistor.fm dashboard.`, status);
this.name = "AuthenticationError";
Object.setPrototypeOf(this, AuthenticationError.prototype);
}
}
/** HTTP 429 — too many requests against the Transistor.fm API. */
export class RateLimitError extends TransistorError {
constructor(detail, status) {
super(`Rate limit error (${status}): ${detail}. Slow down requests to the Transistor.fm API and try again shortly.`, status);
this.name = "RateLimitError";
Object.setPrototypeOf(this, RateLimitError.prototype);
}
}
/** HTTP 400 — malformed or invalid request parameters. */
export class ValidationError extends TransistorError {
constructor(detail, status) {
super(`Validation error (${status}): ${detail}. Check the arguments passed to this tool.`, status);
this.name = "ValidationError";
Object.setPrototypeOf(this, ValidationError.prototype);
}
}
/** HTTP 404 — the requested show, episode, subscriber, or webhook does not exist. */
export class NotFoundError extends TransistorError {
constructor(detail, status) {
super(`Not found (${status}): ${detail}.`, status);
this.name = "NotFoundError";
Object.setPrototypeOf(this, NotFoundError.prototype);
}
}
/** HTTP 5xx — failure on Transistor.fm's side. */
export class ServerError extends TransistorError {
constructor(detail, status) {
super(`Server error (${status}): ${detail}. The Transistor.fm API may be experiencing issues — try again later.`, status);
this.name = "ServerError";
Object.setPrototypeOf(this, ServerError.prototype);
}
}
/**
* Maps an HTTP status code + error detail string to the appropriate typed
* error. Falls back to the base `TransistorError` (preserving the original
* "API error (<status>): <detail>" wording) for status codes outside the
* mapped classes, or when no status is available (e.g. network failures).
*/
export function mapHttpStatusToError(status, detail) {
if (status === 401 || status === 403)
return new AuthenticationError(detail, status);
if (status === 429)
return new RateLimitError(detail, status);
if (status === 400)
return new ValidationError(detail, status);
if (status === 404)
return new NotFoundError(detail, status);
if (status !== undefined && status >= 500)
return new ServerError(detail, status);
return new TransistorError(`API error (${status ?? "unknown"}): ${detail}`, status);
}
/**
* Pulls a human-readable detail string out of an Axios error response body.
*
* Unlike podcastindex-mcp, we have not been able to hit the live Transistor
* API to confirm the exact shape of its error bodies, so this is a best
* effort built from two signals already in this codebase: (1) the
* pre-existing (pre-typed-errors) catch block in tool-handlers.ts read a flat
* `error.response?.data?.message` field, which may have been an incomplete
* guess rather than a confirmed shape; (2) every *successful* response
* shape used throughout api-client.ts and `trimEpisodeResponse` in
* tool-handlers.ts follows the JSON:API convention (`{ data: { attributes:
* ... } }`), and JSON:API's own spec defines errors as `{ errors: [ { status,
* title, detail } ] }` — a strong signal Transistor's error bodies plausibly
* follow the same convention elsewhere in the API surface, even though we
* have not observed it directly.
*
* To stay safe against both possibilities (and anything else), this checks,
* in priority order:
* 1. A plain string body (mirrors podcastindex-mcp, in case Transistor also
* sends bare-text bodies for some error class).
* 2. An object with a `.message` string field (preserves the exact
* behavior of the pre-typed-errors code at tool-handlers.ts ~line 1105).
* 3. An object with an `.errors` array (JSON:API shape) where each entry
* may carry `.detail` or `.title`; multiple entries are joined into one
* string.
* 4. The provided fallback (e.g. Axios's generic "Request failed with
* status code NNN") if none of the above match.
* It never throws on an unexpected shape — an unrecognized body degrades to
* the fallback rather than crashing the error-mapping path itself.
*/
export function extractErrorDetail(responseData, fallback) {
if (typeof responseData === "string" && responseData.length > 0)
return responseData;
if (responseData && typeof responseData === "object") {
const message = responseData.message;
if (typeof message === "string" && message.length > 0)
return message;
const errors = responseData.errors;
if (Array.isArray(errors) && errors.length > 0) {
const parts = errors
.map((e) => {
if (e && typeof e === "object") {
const detail = e.detail;
if (typeof detail === "string" && detail.length > 0)
return detail;
const title = e.title;
if (typeof title === "string" && title.length > 0)
return title;
}
return undefined;
})
.filter((part) => Boolean(part));
if (parts.length > 0)
return parts.join("; ");
}
}
return fallback;
}
+14
-0
import axios from "axios";
import { mapHttpStatusToError, extractErrorDetail } from "./errors.js";
import { toTransistorDate } from "./date-utils.js";

@@ -13,2 +14,15 @@ export class TransistorApiClient {

});
// Centralize HTTP-failure mapping in one place via a response
// interceptor rather than a shared request helper. This repo's ~20
// endpoint methods each call `this.api.get/post/patch/delete` directly
// (unlike podcastindex-mcp, which funnels every call through a private
// `get()` helper) — retrofitting a shared helper here would mean
// touching every method body. An interceptor achieves the same "typed
// errors mapped in exactly one place" goal with a minimal diff.
this.api.interceptors.response.use((res) => res, (error) => {
if (axios.isAxiosError(error)) {
throw mapHttpStatusToError(error.response?.status, extractErrorDetail(error.response?.data, error.message));
}
throw error;
});
}

@@ -15,0 +29,0 @@ async getAuthenticatedUser() {

+1
-1

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

name: "transistor-server",
version: "0.1.0",
version: "0.4.0",
}, {

@@ -21,0 +21,0 @@ capabilities: {

@@ -5,11 +5,17 @@ import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";

import axios from "axios";
import { TransistorError, mapHttpStatusToError, extractErrorDetail } from "./errors.js";
/**
* Extract a concise, human-readable message from an unknown thrown value,
* preferring the Transistor API's status/message when it is an Axios error.
* Extract a concise, human-readable message from an unknown thrown value.
* `TransistorApiClient`'s response interceptor (see api-client.ts) already
* maps HTTP failures to typed errors before they reach this call site, so
* the common case is just reading `.message`. The `axios.isAxiosError`
* branch is a defensive fallback for a raw axios error that somehow
* bypasses the interceptor.
*/
function errorMessage(e) {
if (e instanceof TransistorError)
return e.message;
if (axios.isAxiosError(e)) {
const status = e.response?.status;
const apiMsg = e.response?.data?.error ?? e.message;
return status ? `HTTP ${status}: ${apiMsg}` : apiMsg;
return mapHttpStatusToError(status, extractErrorDetail(e.response?.data, e.message)).message;
}

@@ -954,2 +960,16 @@ return e instanceof Error ? e.message : String(e);

catch (error) {
if (error instanceof McpError)
throw error;
// Typed errors (see ./errors.ts) are thrown by TransistorApiClient's
// response interceptor with a fully-formatted, status-specific message
// already baked in.
if (error instanceof TransistorError) {
return {
content: [{ type: "text", text: error.message }],
isError: true,
};
}
// Fallback for a raw axios error that reaches this layer without
// going through TransistorApiClient's interceptor (kept for backward
// compatibility with the original isError response shape).
if (axios.isAxiosError(error)) {

@@ -956,0 +976,0 @@ return {

{
"name": "transistor-mcp",
"version": "0.3.0",
"description": "MCP server for the Transistor.fm podcast hosting API \u2014 manage shows, episodes, analytics, download summaries, transcripts, subscribers, and webhooks.",
"version": "0.4.0",
"description": "MCP server for the Transistor.fm podcast hosting API — manage shows, episodes, analytics, download summaries, transcripts, subscribers, and webhooks.",
"type": "module",

@@ -42,2 +42,3 @@ "mcpName": "io.github.conorbronsdon/transistor-mcp",

"lint": "tsc --noEmit",
"test": "vitest run",
"watch": "tsc --watch"

@@ -52,3 +53,5 @@ },

"@types/node": "^20.11.24",
"typescript": "^5.3.3"
"nock": "^14.0.16",
"typescript": "^5.3.3",
"vitest": "^4.1.10"
},

@@ -55,0 +58,0 @@ "engines": {

@@ -237,2 +237,13 @@ <div align="center">

### Typed errors
Tool calls that fail against the Transistor.fm API return a status-specific,
human-readable message via `isError: true` rather than a generic Axios error.
Internally, `TransistorApiClient` maps every HTTP failure — authentication
(401/403), rate limiting (429), validation (400), not found (404), and
server errors (5xx) — to a typed error class in a single response
interceptor (see `src/errors.ts`), so the message you see always names the
failure mode and, for auth errors, points at the `TRANSISTOR_API_KEY`
environment variable.
## Important Notes

@@ -239,0 +250,0 @@