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

@shipeasy/mcp

Package Overview
Dependencies
Maintainers
1
Versions
18
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@shipeasy/mcp - npm Package Compare versions

Comparing version
2.5.0
to
2.6.0
+49
-0
dist/__tests__/tools.test.js

@@ -188,2 +188,51 @@ import { describe, it, expect, vi, afterEach } from "vitest";

});
// ── auth_logout: the one tool that can strand its own caller ─────────────────
/**
* Deleting the config file is machine-wide (the CLI and every MCP client share
* it) and cannot be undone from MCP — `auth_login` explicitly refuses on stdio,
* because a browser round-trip needs a terminal. So an agent that calls this
* while troubleshooting takes out its own session plus every other tool's, and
* has to hand a login URL back to a human. It must not be a one-call action.
*/
describe("auth_logout requires explicit confirmation", () => {
afterEach(() => vi.clearAllMocks());
it("refuses without confirm, and does NOT touch the config file", async () => {
const { handleAuthLogout } = await import("../tools/shared/auth.js");
const { clearConfig } = await import("../auth/config.js");
const res = await handleAuthLogout({});
expect(res.isError).toBe(true);
expect(clearConfig).not.toHaveBeenCalled();
const text = JSON.stringify(res.content);
expect(text).toContain("confirm: true");
// The refusal has to redirect the likely intent, or the agent just retries
// with confirm:true and we've achieved nothing.
expect(text).toMatch(/401\/403/);
expect(text).toContain("cannot be restored from MCP");
});
it("refuses a truthy-but-not-true confirm (no accidental coercion)", async () => {
const { handleAuthLogout } = await import("../tools/shared/auth.js");
const { clearConfig } = await import("../auth/config.js");
for (const confirm of ["true", 1, {}]) {
const res = await handleAuthLogout({ confirm });
expect(res.isError, `confirm=${JSON.stringify(confirm)} must be refused`).toBe(true);
}
expect(clearConfig).not.toHaveBeenCalled();
});
it("signs out with confirm: true, and says how to get back in", async () => {
const { handleAuthLogout } = await import("../tools/shared/auth.js");
const { clearConfig } = await import("../auth/config.js");
const res = await handleAuthLogout({ confirm: true });
expect(res.isError).toBeUndefined();
expect(clearConfig).toHaveBeenCalledTimes(1);
expect(JSON.stringify(res.content)).toMatch(/shipeasy login|shipeasy-mcp install/);
});
it("declares confirm as required in its schema", async () => {
const { TOOLS } = await import("../tools/schema.js");
const tool = TOOLS.find((t) => t.name === "auth_logout");
expect(tool.inputSchema.required).toEqual(["confirm"]);
expect(tool.annotations?.destructiveHint).toBe(true);
// The description carries the blast radius — clients surface it verbatim.
expect(tool.description).toContain("not restorable from MCP");
});
});
// ── custom (non-spec) tools ──────────────────────────────────────────────────

@@ -190,0 +239,0 @@ describe("custom tools", () => {

@@ -32,2 +32,11 @@ export interface ShipeasyConfig {

export declare function diagnoseMissingConfig(): Promise<string>;
/**
* Write the session ATOMICALLY — temp file in the same directory, then rename.
*
* `writeFile` truncates first, so a concurrent writer (the CLI and any number of
* MCP clients share this one path) or an interrupted write leaves a torn or
* empty file. Readers then fail to parse it and report "not authenticated",
* which is indistinguishable from a deleted session and just as unrecoverable
* without a browser. `rename` within a directory is atomic.
*/
export declare function writeConfig(cfg: ShipeasyConfig): Promise<void>;

@@ -34,0 +43,0 @@ export declare function clearConfig(): Promise<boolean>;

+19
-2

@@ -1,2 +0,2 @@

import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
import { homedir } from "node:os";

@@ -72,6 +72,23 @@ import { dirname, join } from "node:path";

}
/**
* Write the session ATOMICALLY — temp file in the same directory, then rename.
*
* `writeFile` truncates first, so a concurrent writer (the CLI and any number of
* MCP clients share this one path) or an interrupted write leaves a torn or
* empty file. Readers then fail to parse it and report "not authenticated",
* which is indistinguishable from a deleted session and just as unrecoverable
* without a browser. `rename` within a directory is atomic.
*/
export async function writeConfig(cfg) {
const path = configPath();
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
await writeFile(path, JSON.stringify(cfg, null, 2) + "\n", { mode: 0o600 });
const tmp = `${path}.${process.pid}.tmp`;
try {
await writeFile(tmp, JSON.stringify(cfg, null, 2) + "\n", { mode: 0o600 });
await rename(tmp, path);
}
catch (err) {
await unlink(tmp).catch(() => { });
throw err;
}
}

@@ -78,0 +95,0 @@ export async function clearConfig() {

+1
-1

@@ -137,3 +137,3 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";

if (toolName === "auth_logout")
return handleAuthLogout();
return handleAuthLogout((params.arguments ?? {}));
// Everything else — every CRUD/read/docs surface incl. the i18n admin API

@@ -140,0 +140,0 @@ // (`i18n_profiles_*`/`i18n_keys_*`/`i18n_drafts_list`), alert rules, the

@@ -90,5 +90,20 @@ import { REGISTRY_TOOLS } from "./registry.js";

name: "auth_logout",
description: "Delete ~/.config/shipeasy/config.json. No network call.",
inputSchema: { type: "object", properties: {} },
// Deletes the local CLI token.
description: "Delete ~/.config/shipeasy/config.json — the ONE session shared by the `shipeasy` CLI and " +
"every MCP client on this machine. No network call. **This is not restorable from MCP**: " +
"re-authenticating needs a browser sign-in in a terminal (`shipeasy login`), which this " +
"transport cannot perform, so calling this strands the current task and every other tool " +
"until a human signs in again. Do NOT call it to 'reset' or troubleshoot a failing " +
"call — a 401/403 is fixed by signing in, not by deleting the credential first. Only call " +
"it when the user explicitly asked to sign out, and pass confirm: true to acknowledge that.",
inputSchema: {
type: "object",
properties: {
confirm: {
type: "boolean",
description: "Must be true. Acknowledges that this deletes the machine-wide session and that only a human at a terminal can restore it.",
},
},
required: ["confirm"],
},
// Deletes the local CLI token — machine-wide, and unrecoverable over stdio.
annotations: {

@@ -95,0 +110,0 @@ title: "Auth Logout",

@@ -12,3 +12,16 @@ /**

}>;
export declare function handleAuthLogout(): Promise<{
/**
* `auth_logout` MCP tool handler.
*
* Schema-level `required: ["confirm"]` is advisory — a client that ignores it
* still reaches this handler — so the guard is enforced here too. The cost of a
* stray call is high and asymmetric: the deleted file is the session the CLI and
* every MCP client share, and `auth_login` cannot run over stdio, so nothing on
* this transport can put it back. A refusal costs one retry; a wrong deletion
* costs a human a browser round-trip.
*/
export declare function handleAuthLogout(args?: {
confirm?: unknown;
}): Promise<{
isError: boolean;
content: {

@@ -18,2 +31,8 @@ type: "text";

}[];
} | {
content: {
type: "text";
text: string;
}[];
isError?: undefined;
}>;

@@ -30,7 +30,39 @@ import { configPath, readConfig } from "../../auth/config.js";

}
export async function handleAuthLogout() {
/**
* `auth_logout` MCP tool handler.
*
* Schema-level `required: ["confirm"]` is advisory — a client that ignores it
* still reaches this handler — so the guard is enforced here too. The cost of a
* stray call is high and asymmetric: the deleted file is the session the CLI and
* every MCP client share, and `auth_login` cannot run over stdio, so nothing on
* this transport can put it back. A refusal costs one retry; a wrong deletion
* costs a human a browser round-trip.
*/
export async function handleAuthLogout(args = {}) {
if (args.confirm !== true) {
return {
isError: true,
content: [
{
type: "text",
text: `Refused: auth_logout deletes ${configPath()}, the session shared by the ` +
"`shipeasy` CLI and every MCP client on this machine, and it cannot be " +
"restored from MCP (browser sign-in only runs in a terminal).\n\n" +
"If a call is failing with 401/403, sign in — do not log out first; the " +
"credential you would delete is the one you still need.\n\n" +
"If the user explicitly asked to sign out, call again with confirm: true.",
},
],
};
}
await runLogout();
return {
content: [{ type: "text", text: "Signed out locally." }],
content: [
{
type: "text",
text: "Signed out locally. Restoring the session needs `shipeasy login` (or " +
"`shipeasy-mcp install`) in a terminal — a browser sign-in this transport cannot do.",
},
],
};
}
{
"name": "@shipeasy/mcp",
"version": "2.5.0",
"version": "2.6.0",
"description": "Feature flags, A/B experiments, kill switches, dynamic config & i18n — Shipeasy MCP server.",

@@ -5,0 +5,0 @@ "mcpName": "ai.shipeasy/mcp",