younium-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # younium-mcp
2
+
3
+ MCP server for the [Younium](https://younium.com) subscription management API. Exposes all 159 Younium API operations as tools for Claude Desktop and other MCP clients.
4
+
5
+ ## Setup for Claude Desktop
6
+
7
+ 1. Find your Claude Desktop config file:
8
+ - **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
9
+ - **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
10
+
11
+ 2. Add the `younium` entry to `mcpServers` (merge with any existing config):
12
+
13
+ ```json
14
+ {
15
+ "mcpServers": {
16
+ "younium": {
17
+ "command": "npx",
18
+ "args": ["-y", "younium-mcp"],
19
+ "env": {
20
+ "YOUNIUM_USERNAME": "user@yourcompany.com",
21
+ "YOUNIUM_PASSWORD": "your-younium-password",
22
+ "YOUNIUM_CLIENT_ID": "your-client-id",
23
+ "YOUNIUM_CLIENT_SECRET": "your-client-secret"
24
+ }
25
+ }
26
+ }
27
+ }
28
+ ```
29
+
30
+ 3. Restart Claude Desktop. You should now be able to ask things like:
31
+ - "List all active subscriptions in Younium"
32
+ - "Get account details for customer X"
33
+ - "Show me invoices from the past 30 days"
34
+ - "Create a new quote for account Y"
35
+
36
+ ## Credentials
37
+
38
+ | Env var | Description |
39
+ |---|---|
40
+ | `YOUNIUM_USERNAME` | Your Younium login email |
41
+ | `YOUNIUM_PASSWORD` | Your Younium password |
42
+ | `YOUNIUM_CLIENT_ID` | OAuth2 client ID (from Younium settings) |
43
+ | `YOUNIUM_CLIENT_SECRET` | OAuth2 client secret |
44
+ | `YOUNIUM_LEGAL_ENTITY` | *(optional)* Legal entity ID for multi-tenant setups |
45
+
46
+ ## Available tools
47
+
48
+ All 159 Younium API v2.1 endpoints are exposed. Resources include:
49
+
50
+ - **Accounts** — CRUD, payment details, GoCardless/Stripe requests
51
+ - **Subscriptions** — CRUD, amend, cancel, move, charge
52
+ - **Invoices** — list, get, create, post, void, credit notes
53
+ - **Payments** — list, get, create, post
54
+ - **Products / SimpleProducts** — CRUD
55
+ - **Orders / SalesOrders** — CRUD, charges, versions
56
+ - **Quotes** — CRUD, convert to order
57
+ - **Reports** — list, get data
58
+ - **Usage / Measurements** — list, get, import
59
+ - **Webhooks** — list, get, create, delete
60
+ - **Users** — list, get, invite, deactivate
61
+ - **Journals** — list, get, book, reverse
62
+ - And more: Bookings, Metrics, ChartOfAccounts, Currency, PaymentTerms, TaxTemplates, etc.
63
+
64
+ ## Requirements
65
+
66
+ - Node.js 18+
package/dist/auth.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function getToken(): Promise<string>;
package/dist/auth.js ADDED
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getToken = getToken;
4
+ const TOKEN_URL = "https://auth.younium.com/connect/token";
5
+ const REFRESH_BUFFER_SECS = 60;
6
+ let cached = null;
7
+ function getEnv(name) {
8
+ const val = process.env[name];
9
+ if (!val)
10
+ throw new Error(`Missing required env var: ${name}`);
11
+ return val;
12
+ }
13
+ async function getToken() {
14
+ const now = Date.now() / 1000;
15
+ if (cached && cached.expiresAt > now + REFRESH_BUFFER_SECS) {
16
+ return cached.value;
17
+ }
18
+ const body = new URLSearchParams({
19
+ grant_type: "password",
20
+ scope: "youniumapi",
21
+ username: getEnv("YOUNIUM_USERNAME"),
22
+ password: getEnv("YOUNIUM_PASSWORD"),
23
+ client_id: getEnv("YOUNIUM_CLIENT_ID"),
24
+ client_secret: getEnv("YOUNIUM_CLIENT_SECRET"),
25
+ });
26
+ const res = await fetch(TOKEN_URL, {
27
+ method: "POST",
28
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
29
+ body: body.toString(),
30
+ });
31
+ if (!res.ok) {
32
+ const text = await res.text();
33
+ throw new Error(`Younium auth failed (${res.status}): ${text}`);
34
+ }
35
+ const data = (await res.json());
36
+ cached = { value: data.access_token, expiresAt: now + data.expires_in };
37
+ return cached.value;
38
+ }
@@ -0,0 +1 @@
1
+ export declare function request(method: string, path: string, query: Record<string, string>, body: unknown | null, idempotencyKey?: string): Promise<unknown>;
package/dist/client.js ADDED
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.request = request;
4
+ const auth_js_1 = require("./auth.js");
5
+ const BASE_URL = "https://api.younium.com";
6
+ const API_VERSION = "2.1";
7
+ async function request(method, path, query, body, idempotencyKey) {
8
+ const token = await (0, auth_js_1.getToken)();
9
+ const url = new URL(BASE_URL + path);
10
+ for (const [k, v] of Object.entries(query)) {
11
+ if (v !== undefined && v !== null && v !== "") {
12
+ url.searchParams.set(k, String(v));
13
+ }
14
+ }
15
+ const headers = {
16
+ Authorization: `Bearer ${token}`,
17
+ "api-version": API_VERSION,
18
+ Accept: "application/json",
19
+ };
20
+ if (body !== null) {
21
+ headers["Content-Type"] = "application/json";
22
+ }
23
+ if (idempotencyKey) {
24
+ headers["younium-idempotency-key"] = idempotencyKey;
25
+ }
26
+ const legalEntity = process.env.YOUNIUM_LEGAL_ENTITY;
27
+ if (legalEntity) {
28
+ headers["legal-entity"] = legalEntity;
29
+ }
30
+ const res = await fetch(url.toString(), {
31
+ method: method.toUpperCase(),
32
+ headers,
33
+ body: body !== null ? JSON.stringify(body) : undefined,
34
+ });
35
+ const text = await res.text();
36
+ if (!res.ok) {
37
+ return { error: true, status: res.status, body: text };
38
+ }
39
+ if (!text)
40
+ return { success: true };
41
+ try {
42
+ return JSON.parse(text);
43
+ }
44
+ catch {
45
+ return text;
46
+ }
47
+ }
@@ -0,0 +1,2 @@
1
+ import type { ToolDefinition } from "./spec-loader.js";
2
+ export declare function execute(tool: ToolDefinition, args: Record<string, unknown>): Promise<string>;
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.execute = execute;
4
+ const client_js_1 = require("./client.js");
5
+ async function execute(tool, args) {
6
+ // Substitute path parameters
7
+ let urlPath = tool.pathTemplate;
8
+ for (const param of tool.pathParams) {
9
+ const val = args[param];
10
+ if (val === undefined || val === null) {
11
+ return JSON.stringify({ error: `Missing required path parameter: ${param}` });
12
+ }
13
+ urlPath = urlPath.replace(`{${param}}`, encodeURIComponent(String(val)));
14
+ }
15
+ // Collect query params
16
+ const query = {};
17
+ for (const param of tool.queryParams) {
18
+ const val = args[param];
19
+ if (val !== undefined && val !== null) {
20
+ query[param] = String(val);
21
+ }
22
+ }
23
+ // Build body: everything that isn't a path or query param
24
+ let body = null;
25
+ if (tool.hasBody) {
26
+ const paramSet = new Set([...tool.pathParams, ...tool.queryParams]);
27
+ const bodyFields = {};
28
+ for (const [k, v] of Object.entries(args)) {
29
+ if (!paramSet.has(k) && v !== undefined) {
30
+ bodyFields[k] = v;
31
+ }
32
+ }
33
+ body = Object.keys(bodyFields).length > 0 ? bodyFields : null;
34
+ }
35
+ const idempotencyKey = typeof args["younium-idempotency-key"] === "string"
36
+ ? args["younium-idempotency-key"]
37
+ : undefined;
38
+ try {
39
+ const result = await (0, client_js_1.request)(tool.method, urlPath, query, body, idempotencyKey);
40
+ return JSON.stringify(result, null, 2);
41
+ }
42
+ catch (err) {
43
+ return JSON.stringify({ error: String(err) });
44
+ }
45
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
5
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
6
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
7
+ const spec_loader_js_1 = require("./spec-loader.js");
8
+ const executor_js_1 = require("./executor.js");
9
+ const tools = (0, spec_loader_js_1.loadTools)();
10
+ // Build a fast lookup map
11
+ const toolMap = new Map(tools.map((t) => [t.name, t]));
12
+ const server = new index_js_1.Server({ name: "younium-mcp", version: "1.0.0" }, { capabilities: { tools: {} } });
13
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
14
+ tools: tools.map((t) => ({
15
+ name: t.name,
16
+ description: t.description,
17
+ inputSchema: t.inputSchema,
18
+ })),
19
+ }));
20
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
21
+ const { name, arguments: args = {} } = req.params;
22
+ const tool = toolMap.get(name);
23
+ if (!tool) {
24
+ return {
25
+ content: [{ type: "text", text: JSON.stringify({ error: `Unknown tool: ${name}` }) }],
26
+ isError: true,
27
+ };
28
+ }
29
+ const result = await (0, executor_js_1.execute)(tool, args);
30
+ return { content: [{ type: "text", text: result }] };
31
+ });
32
+ async function main() {
33
+ const transport = new stdio_js_1.StdioServerTransport();
34
+ await server.connect(transport);
35
+ }
36
+ main().catch((err) => {
37
+ process.stderr.write(`Fatal: ${err}\n`);
38
+ process.exit(1);
39
+ });
@@ -0,0 +1,27 @@
1
+ interface JsonSchema {
2
+ type?: string;
3
+ format?: string;
4
+ description?: string;
5
+ nullable?: boolean;
6
+ properties?: Record<string, JsonSchema>;
7
+ items?: JsonSchema;
8
+ required?: string[];
9
+ additionalProperties?: JsonSchema | boolean;
10
+ allOf?: JsonSchema[];
11
+ oneOf?: JsonSchema[];
12
+ anyOf?: JsonSchema[];
13
+ $ref?: string;
14
+ enum?: unknown[];
15
+ }
16
+ export interface ToolDefinition {
17
+ name: string;
18
+ description: string;
19
+ inputSchema: JsonSchema;
20
+ method: string;
21
+ pathTemplate: string;
22
+ pathParams: string[];
23
+ queryParams: string[];
24
+ hasBody: boolean;
25
+ }
26
+ export declare function loadTools(): ToolDefinition[];
27
+ export {};
@@ -0,0 +1,196 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.loadTools = loadTools;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const HTTP_METHODS = ["get", "post", "put", "patch", "delete"];
40
+ function operationIdToName(operationId) {
41
+ return operationId
42
+ .replace(/[^a-zA-Z0-9]+/g, "_")
43
+ .replace(/([a-z])([A-Z])/g, "$1_$2")
44
+ .toLowerCase()
45
+ .replace(/^_+|_+$/g, "");
46
+ }
47
+ function resolveRef(ref, spec) {
48
+ const parts = ref.replace(/^#\//, "").split("/");
49
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
50
+ let cur = spec;
51
+ for (const p of parts) {
52
+ cur = cur[p];
53
+ if (cur === undefined)
54
+ return {};
55
+ }
56
+ return cur;
57
+ }
58
+ function resolveSchema(schema, spec, depth = 0) {
59
+ if (depth > 6)
60
+ return { type: "object" };
61
+ if (schema.$ref) {
62
+ return resolveSchema(resolveRef(schema.$ref, spec), spec, depth + 1);
63
+ }
64
+ const out = { ...schema };
65
+ delete out.$ref;
66
+ if (out.nullable) {
67
+ delete out.nullable;
68
+ }
69
+ if (out.properties) {
70
+ const resolved = {};
71
+ for (const [k, v] of Object.entries(out.properties)) {
72
+ resolved[k] = resolveSchema(v, spec, depth + 1);
73
+ }
74
+ out.properties = resolved;
75
+ }
76
+ if (out.items) {
77
+ out.items = resolveSchema(out.items, spec, depth + 1);
78
+ }
79
+ if (out.allOf) {
80
+ const merged = mergeAllOf(out.allOf, spec, depth);
81
+ delete out.allOf;
82
+ Object.assign(out, merged);
83
+ }
84
+ if (out.oneOf) {
85
+ out.oneOf = out.oneOf.map((s) => resolveSchema(s, spec, depth + 1));
86
+ }
87
+ if (out.anyOf) {
88
+ out.anyOf = out.anyOf.map((s) => resolveSchema(s, spec, depth + 1));
89
+ }
90
+ return out;
91
+ }
92
+ function mergeAllOf(schemas, spec, depth) {
93
+ const merged = { type: "object", properties: {}, required: [] };
94
+ for (const s of schemas) {
95
+ const resolved = resolveSchema(s, spec, depth + 1);
96
+ if (resolved.properties) {
97
+ Object.assign(merged.properties, resolved.properties);
98
+ }
99
+ if (resolved.required) {
100
+ merged.required = [...(merged.required ?? []), ...resolved.required];
101
+ }
102
+ if (resolved.type && resolved.type !== "object") {
103
+ merged.type = resolved.type;
104
+ }
105
+ }
106
+ if (!merged.properties || Object.keys(merged.properties).length === 0) {
107
+ delete merged.properties;
108
+ }
109
+ if (!merged.required || merged.required.length === 0) {
110
+ delete merged.required;
111
+ }
112
+ return merged;
113
+ }
114
+ function paramSchema(param, spec) {
115
+ const base = param.schema ? resolveSchema(param.schema, spec) : { type: "string" };
116
+ if (param.description) {
117
+ base.description = param.description;
118
+ }
119
+ return base;
120
+ }
121
+ function loadTools() {
122
+ const specPath = path.join(__dirname, "..", "spec", "youniumv2-prod.json");
123
+ const raw = fs.readFileSync(specPath, "utf-8");
124
+ const spec = JSON.parse(raw);
125
+ const tools = [];
126
+ const seen = new Set();
127
+ for (const [pathTemplate, methods] of Object.entries(spec.paths)) {
128
+ for (const method of HTTP_METHODS) {
129
+ const op = methods[method];
130
+ if (!op)
131
+ continue;
132
+ const rawId = op.operationId ?? `${method}_${pathTemplate.replace(/[^a-zA-Z0-9]/g, "_")}`;
133
+ let name = operationIdToName(rawId);
134
+ // Deduplicate
135
+ if (seen.has(name)) {
136
+ name = `${name}_${method}`;
137
+ }
138
+ seen.add(name);
139
+ const description = [op.summary, op.description]
140
+ .filter(Boolean)
141
+ .join(" — ")
142
+ .slice(0, 400);
143
+ const params = (op.parameters ?? []).filter((p) => p.in !== "header");
144
+ const pathParams = params
145
+ .filter((p) => p.in === "path")
146
+ .map((p) => p.name);
147
+ const queryParams = params
148
+ .filter((p) => p.in === "query")
149
+ .map((p) => p.name);
150
+ // Build combined input schema
151
+ const properties = {};
152
+ const required = [];
153
+ for (const p of params) {
154
+ properties[p.name] = paramSchema(p, spec);
155
+ if (p.required)
156
+ required.push(p.name);
157
+ }
158
+ let hasBody = false;
159
+ if (op.requestBody?.content) {
160
+ const jsonContent = op.requestBody.content["application/json"] ??
161
+ op.requestBody.content["application/merge-patch+json"] ??
162
+ Object.values(op.requestBody.content)[0];
163
+ if (jsonContent?.schema) {
164
+ const bodySchema = resolveSchema(jsonContent.schema, spec);
165
+ if (bodySchema.properties) {
166
+ for (const [k, v] of Object.entries(bodySchema.properties)) {
167
+ properties[k] = v;
168
+ }
169
+ if (bodySchema.required) {
170
+ required.push(...bodySchema.required.filter((r) => !required.includes(r)));
171
+ }
172
+ }
173
+ else {
174
+ // Scalar or array body — wrap it
175
+ properties["body"] = bodySchema;
176
+ }
177
+ hasBody = true;
178
+ }
179
+ }
180
+ const inputSchema = { type: "object", properties };
181
+ if (required.length > 0)
182
+ inputSchema.required = required;
183
+ tools.push({
184
+ name,
185
+ description,
186
+ inputSchema,
187
+ method,
188
+ pathTemplate,
189
+ pathParams,
190
+ queryParams,
191
+ hasBody,
192
+ });
193
+ }
194
+ }
195
+ return tools;
196
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "younium-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for the Younium subscription management API",
5
+ "main": "dist/index.js",
6
+ "bin": "dist/index.js",
7
+ "files": [
8
+ "dist",
9
+ "spec"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "dev": "ts-node src/index.ts",
14
+ "prepublishOnly": "npm run build"
15
+ },
16
+ "dependencies": {
17
+ "@modelcontextprotocol/sdk": "^1.12.0",
18
+ "zod": "^3.23.8"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^22.0.0",
22
+ "typescript": "^5.5.0"
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "keywords": [
31
+ "younium",
32
+ "mcp",
33
+ "claude",
34
+ "subscription-management"
35
+ ]
36
+ }