fastspring-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 +55 -0
- package/fastspring-api.ts +101 -0
- package/index.ts +147 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# FastSpring MCP Server
|
|
2
|
+
|
|
3
|
+
MCP server that connects Claude (or any MCP client) to the FastSpring API. Look up subscriptions, accounts, and webhook events through natural language.
|
|
4
|
+
|
|
5
|
+
## Tools
|
|
6
|
+
|
|
7
|
+
| Tool | Description |
|
|
8
|
+
|------|-------------|
|
|
9
|
+
| `get_subscription` | Get subscription details by ID |
|
|
10
|
+
| `list_subscriptions` | List/filter subscriptions by account, status, date range |
|
|
11
|
+
| `list_subscription_entries` | Payment history for a subscription |
|
|
12
|
+
| `search_accounts` | Find accounts by email |
|
|
13
|
+
| `get_account` | Get account details by ID |
|
|
14
|
+
| `list_events` | List webhook events (up to 30 days) |
|
|
15
|
+
| `get_event` | Get full event payload by ID |
|
|
16
|
+
|
|
17
|
+
## Setup
|
|
18
|
+
|
|
19
|
+
Requires Node.js 22+ and FastSpring API credentials.
|
|
20
|
+
|
|
21
|
+
Set env vars in your shell profile:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
export FASTSPRING_API_USERNAME=your_username
|
|
25
|
+
export FASTSPRING_API_PASSWORD=your_password
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Claude Code
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
claude mcp add fastspring -- npx fastspring-mcp
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Claude Desktop
|
|
35
|
+
|
|
36
|
+
Add to `claude_desktop_config.json`:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"mcpServers": {
|
|
41
|
+
"fastspring": {
|
|
42
|
+
"command": "npx",
|
|
43
|
+
"args": ["fastspring-mcp"],
|
|
44
|
+
"env": {
|
|
45
|
+
"FASTSPRING_API_USERNAME": "your_username",
|
|
46
|
+
"FASTSPRING_API_PASSWORD": "your_password"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## License
|
|
54
|
+
|
|
55
|
+
MIT
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
const BASE_URL = "https://api.fastspring.com";
|
|
2
|
+
|
|
3
|
+
let authHeader: string | undefined;
|
|
4
|
+
|
|
5
|
+
export function initAuth(username: string, password: string): void {
|
|
6
|
+
authHeader = "Basic " + Buffer.from(`${username}:${password}`).toString("base64");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Validate ID parameters to prevent path traversal
|
|
10
|
+
function validateId(id: string, label: string): void {
|
|
11
|
+
if (!/^[\w-]+$/.test(id)) {
|
|
12
|
+
throw new Error(`Invalid ${label}: must be alphanumeric (got "${id}")`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function request(path: string, params?: Record<string, string>): Promise<unknown> {
|
|
17
|
+
if (!authHeader) throw new Error("FastSpring API not initialized — call initAuth() first.");
|
|
18
|
+
const url = new URL(path, BASE_URL);
|
|
19
|
+
if (params) {
|
|
20
|
+
for (const [k, v] of Object.entries(params)) {
|
|
21
|
+
if (v !== undefined && v !== "") url.searchParams.set(k, v);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const res = await fetch(url.toString(), {
|
|
26
|
+
headers: {
|
|
27
|
+
Authorization: authHeader,
|
|
28
|
+
"User-Agent": "fastspring-mcp/1.0",
|
|
29
|
+
Accept: "application/json",
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (res.status === 429) {
|
|
34
|
+
throw new Error("FastSpring rate limit exceeded (250/min). Wait a moment and retry.");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!res.ok) {
|
|
38
|
+
const body = await res.text();
|
|
39
|
+
throw new Error(`FastSpring API error (${res.status})`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return res.json();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// --- Subscriptions ---
|
|
46
|
+
|
|
47
|
+
export async function getSubscription(subscriptionId: string): Promise<unknown> {
|
|
48
|
+
validateId(subscriptionId, "subscriptionId");
|
|
49
|
+
return request(`/subscriptions/${subscriptionId}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function listSubscriptions(filters: {
|
|
53
|
+
accountId?: string;
|
|
54
|
+
status?: string;
|
|
55
|
+
begin?: string;
|
|
56
|
+
end?: string;
|
|
57
|
+
limit?: number;
|
|
58
|
+
}): Promise<unknown> {
|
|
59
|
+
const params: Record<string, string> = {};
|
|
60
|
+
if (filters.accountId) params.accountId = filters.accountId;
|
|
61
|
+
if (filters.status) params.status = filters.status;
|
|
62
|
+
if (filters.begin) params.begin = filters.begin;
|
|
63
|
+
if (filters.end) params.end = filters.end;
|
|
64
|
+
if (filters.limit) params.limit = String(filters.limit);
|
|
65
|
+
return request("/subscriptions", params);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function listSubscriptionEntries(subscriptionId: string): Promise<unknown> {
|
|
69
|
+
validateId(subscriptionId, "subscriptionId");
|
|
70
|
+
return request(`/subscriptions/${subscriptionId}/entries`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// --- Accounts ---
|
|
74
|
+
|
|
75
|
+
export async function searchAccounts(email: string): Promise<unknown> {
|
|
76
|
+
if (!email.trim()) throw new Error("email is required for account search");
|
|
77
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) throw new Error("invalid email format");
|
|
78
|
+
return request("/accounts", { email });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function getAccount(accountId: string): Promise<unknown> {
|
|
82
|
+
validateId(accountId, "accountId");
|
|
83
|
+
return request(`/accounts/${accountId}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// --- Events ---
|
|
87
|
+
|
|
88
|
+
export async function listEvents(filters: {
|
|
89
|
+
days?: number;
|
|
90
|
+
type?: string;
|
|
91
|
+
}): Promise<unknown> {
|
|
92
|
+
const params: Record<string, string> = {};
|
|
93
|
+
params.days = String(filters.days ?? 7);
|
|
94
|
+
if (filters.type) params.type = filters.type;
|
|
95
|
+
return request("/events", params);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function getEvent(eventId: string): Promise<unknown> {
|
|
99
|
+
validateId(eventId, "eventId");
|
|
100
|
+
return request(`/events/${eventId}`);
|
|
101
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node --experimental-strip-types
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import {
|
|
6
|
+
initAuth,
|
|
7
|
+
getSubscription,
|
|
8
|
+
listSubscriptions,
|
|
9
|
+
listSubscriptionEntries,
|
|
10
|
+
searchAccounts,
|
|
11
|
+
getAccount,
|
|
12
|
+
listEvents,
|
|
13
|
+
getEvent,
|
|
14
|
+
} from "./fastspring-api.ts";
|
|
15
|
+
|
|
16
|
+
// --- Init auth from env ---
|
|
17
|
+
const username = process.env.FASTSPRING_API_USERNAME;
|
|
18
|
+
const password = process.env.FASTSPRING_API_PASSWORD;
|
|
19
|
+
|
|
20
|
+
if (!username || !password) {
|
|
21
|
+
console.error("Missing FASTSPRING_API_USERNAME or FASTSPRING_API_PASSWORD env vars");
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
initAuth(username, password);
|
|
26
|
+
|
|
27
|
+
// --- Create server ---
|
|
28
|
+
const server = new McpServer({
|
|
29
|
+
name: "fastspring",
|
|
30
|
+
version: "1.0.0",
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// --- Helper to format tool responses ---
|
|
34
|
+
function ok(data: unknown): { content: Array<{ type: "text"; text: string }> } {
|
|
35
|
+
return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function err(error: unknown): { content: Array<{ type: "text"; text: string }>; isError: true } {
|
|
39
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
40
|
+
return { content: [{ type: "text" as const, text: message }], isError: true };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// --- Register tools ---
|
|
44
|
+
|
|
45
|
+
server.tool(
|
|
46
|
+
"get_subscription",
|
|
47
|
+
"Get full subscription details by FastSpring subscription ID. Returns status, product, next charge date, pricing, account, and tags (referrer = LinkStorm user ID).",
|
|
48
|
+
{ subscriptionId: z.string().describe("FastSpring subscription ID") },
|
|
49
|
+
async ({ subscriptionId }) => {
|
|
50
|
+
try {
|
|
51
|
+
return ok(await getSubscription(subscriptionId));
|
|
52
|
+
} catch (e) {
|
|
53
|
+
return err(e);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
server.tool(
|
|
59
|
+
"list_subscriptions",
|
|
60
|
+
"List subscriptions with optional filters. Use accountId to find all subs for a customer.",
|
|
61
|
+
{
|
|
62
|
+
accountId: z.string().optional().describe("FastSpring account ID"),
|
|
63
|
+
status: z.enum(["active", "canceled", "deactivated", "overdue", "trial"]).optional().describe("Subscription status filter"),
|
|
64
|
+
begin: z.string().optional().describe("Start date (YYYY-MM-DD)"),
|
|
65
|
+
end: z.string().optional().describe("End date (YYYY-MM-DD)"),
|
|
66
|
+
limit: z.number().min(1).max(100).optional().default(25).describe("Max results (default 25, max 100)"),
|
|
67
|
+
},
|
|
68
|
+
async (filters) => {
|
|
69
|
+
try {
|
|
70
|
+
return ok(await listSubscriptions(filters));
|
|
71
|
+
} catch (e) {
|
|
72
|
+
return err(e);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
server.tool(
|
|
78
|
+
"list_subscription_entries",
|
|
79
|
+
"Get charge/payment history for a subscription. Shows successful charges, failed payments, refunds. Use to distinguish voluntary churn (canceled) from involuntary churn (payment failure).",
|
|
80
|
+
{ subscriptionId: z.string().describe("FastSpring subscription ID") },
|
|
81
|
+
async ({ subscriptionId }) => {
|
|
82
|
+
try {
|
|
83
|
+
return ok(await listSubscriptionEntries(subscriptionId));
|
|
84
|
+
} catch (e) {
|
|
85
|
+
return err(e);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
server.tool(
|
|
91
|
+
"search_accounts",
|
|
92
|
+
"Search FastSpring accounts by email. This is the entry point for debugging — you typically start from a customer email, not a FastSpring ID.",
|
|
93
|
+
{ email: z.string().describe("Customer email to search for") },
|
|
94
|
+
async ({ email }) => {
|
|
95
|
+
try {
|
|
96
|
+
return ok(await searchAccounts(email));
|
|
97
|
+
} catch (e) {
|
|
98
|
+
return err(e);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
server.tool(
|
|
104
|
+
"get_account",
|
|
105
|
+
"Get full account details by FastSpring account ID. Returns email, name, address, and associated subscriptions.",
|
|
106
|
+
{ accountId: z.string().describe("FastSpring account ID") },
|
|
107
|
+
async ({ accountId }) => {
|
|
108
|
+
try {
|
|
109
|
+
return ok(await getAccount(accountId));
|
|
110
|
+
} catch (e) {
|
|
111
|
+
return err(e);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
server.tool(
|
|
117
|
+
"list_events",
|
|
118
|
+
"List FastSpring webhook events. Use for webhook gap analysis — comparing what FastSpring sent vs what Symfony processed. Note: 'processed' means delivered to your endpoint, NOT successfully handled by your code.",
|
|
119
|
+
{
|
|
120
|
+
days: z.number().min(1).max(30).optional().default(7).describe("Number of days to look back (max 30, default 7)"),
|
|
121
|
+
type: z.string().optional().describe("Event type filter, e.g. 'subscription.deactivated', 'subscription.activated'"),
|
|
122
|
+
},
|
|
123
|
+
async (filters) => {
|
|
124
|
+
try {
|
|
125
|
+
return ok(await listEvents(filters));
|
|
126
|
+
} catch (e) {
|
|
127
|
+
return err(e);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
server.tool(
|
|
133
|
+
"get_event",
|
|
134
|
+
"Get full webhook event payload by event ID. Use after list_events to inspect the complete data FastSpring sent.",
|
|
135
|
+
{ eventId: z.string().describe("FastSpring event ID") },
|
|
136
|
+
async ({ eventId }) => {
|
|
137
|
+
try {
|
|
138
|
+
return ok(await getEvent(eventId));
|
|
139
|
+
} catch (e) {
|
|
140
|
+
return err(e);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// --- Start server ---
|
|
146
|
+
const transport = new StdioServerTransport();
|
|
147
|
+
await server.connect(transport);
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fastspring-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for the FastSpring API — manage subscriptions, accounts, and webhook events from Claude",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"fastspring-mcp": "index.ts"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.ts",
|
|
11
|
+
"fastspring-api.ts"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"start": "node --experimental-strip-types index.ts",
|
|
15
|
+
"typecheck": "tsc --noEmit"
|
|
16
|
+
},
|
|
17
|
+
"keywords": ["mcp", "fastspring", "claude", "mcp-server"],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/ssv445/fastspring-mcp-server.git"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
28
|
+
"zod": "^3.24.4"
|
|
29
|
+
}
|
|
30
|
+
}
|