anyapi-mcp-server 1.1.1

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.
@@ -0,0 +1,138 @@
1
+ import { isScalarType, isObjectType, isListType, isNonNullType, } from "graphql";
2
+ const MAX_DEPTH = 2;
3
+ const MAX_FIELDS_PER_LEVEL = 8;
4
+ function unwrapType(type) {
5
+ if (isNonNullType(type))
6
+ return type.ofType;
7
+ return type;
8
+ }
9
+ function getScalarFieldNames(type) {
10
+ const fields = type.getFields();
11
+ return Object.keys(fields).filter((name) => {
12
+ const fieldType = unwrapType(fields[name].type);
13
+ return isScalarType(fieldType);
14
+ });
15
+ }
16
+ function buildDepthLimitedQuery(type, maxDepth, depth = 0) {
17
+ if (depth > maxDepth)
18
+ return null;
19
+ const fields = type.getFields();
20
+ const parts = [];
21
+ let count = 0;
22
+ for (const [name, field] of Object.entries(fields)) {
23
+ if (count >= MAX_FIELDS_PER_LEVEL)
24
+ break;
25
+ const fieldType = unwrapType(field.type);
26
+ if (isScalarType(fieldType)) {
27
+ parts.push(name);
28
+ count++;
29
+ }
30
+ else if (isObjectType(fieldType) && depth < maxDepth) {
31
+ const nested = buildDepthLimitedQuery(fieldType, maxDepth, depth + 1);
32
+ if (nested) {
33
+ parts.push(`${name} ${nested}`);
34
+ count++;
35
+ }
36
+ }
37
+ else if (isListType(fieldType)) {
38
+ const elementType = unwrapType(fieldType.ofType);
39
+ if (isScalarType(elementType)) {
40
+ parts.push(name);
41
+ count++;
42
+ }
43
+ else if (isObjectType(elementType) && depth < maxDepth) {
44
+ const nested = buildDepthLimitedQuery(elementType, maxDepth, depth + 1);
45
+ if (nested) {
46
+ parts.push(`${name} ${nested}`);
47
+ count++;
48
+ }
49
+ }
50
+ }
51
+ }
52
+ return parts.length > 0 ? `{ ${parts.join(" ")} }` : null;
53
+ }
54
+ export function generateSuggestions(schema) {
55
+ const suggestions = [];
56
+ const queryType = schema.getQueryType();
57
+ if (!queryType)
58
+ return suggestions;
59
+ const fields = queryType.getFields();
60
+ const fieldNames = Object.keys(fields);
61
+ // Suggestion 1: All scalar fields at root
62
+ const scalarFields = fieldNames.filter((name) => {
63
+ const type = unwrapType(fields[name].type);
64
+ return isScalarType(type);
65
+ });
66
+ if (scalarFields.length > 0) {
67
+ const selected = scalarFields.slice(0, MAX_FIELDS_PER_LEVEL);
68
+ suggestions.push({
69
+ name: "All top-level scalar fields",
70
+ query: `{ ${selected.join(" ")} }`,
71
+ description: `Returns ${selected.join(", ")}`,
72
+ });
73
+ }
74
+ // Suggestion 2: For each list field, suggest with basic subfields
75
+ for (const [name, field] of Object.entries(fields)) {
76
+ const type = unwrapType(field.type);
77
+ if (isListType(type)) {
78
+ const elementType = unwrapType(type.ofType);
79
+ if (isObjectType(elementType)) {
80
+ const subfields = getScalarFieldNames(elementType).slice(0, MAX_FIELDS_PER_LEVEL);
81
+ if (subfields.length > 0) {
82
+ suggestions.push({
83
+ name: `List ${name} with basic fields`,
84
+ query: `{ ${name} { ${subfields.join(" ")} } }`,
85
+ description: `Fetches ${subfields.join(", ")} for each item in ${name}`,
86
+ });
87
+ }
88
+ }
89
+ }
90
+ }
91
+ // Suggestion 3: Array response pattern (items + _count)
92
+ if (fields["items"] && fields["_count"]) {
93
+ const itemsType = unwrapType(fields["items"].type);
94
+ if (isListType(itemsType)) {
95
+ const elementType = unwrapType(itemsType.ofType);
96
+ if (isObjectType(elementType)) {
97
+ const subfields = getScalarFieldNames(elementType).slice(0, 6);
98
+ if (subfields.length > 0) {
99
+ suggestions.push({
100
+ name: "Items with count",
101
+ query: `{ items { ${subfields.join(" ")} } _count }`,
102
+ description: `Array response: fetches ${subfields.join(", ")} with total count`,
103
+ });
104
+ }
105
+ }
106
+ }
107
+ }
108
+ // Suggestion 4: Full depth-2 query
109
+ const depth2Query = buildDepthLimitedQuery(queryType, MAX_DEPTH);
110
+ if (depth2Query) {
111
+ suggestions.push({
112
+ name: "Full query (depth 2)",
113
+ query: depth2Query,
114
+ description: "All fields up to 2 levels deep, including nested objects",
115
+ });
116
+ }
117
+ // Suggestion 5: Mutation queries
118
+ const mutationType = schema.getMutationType();
119
+ if (mutationType) {
120
+ const mutFields = mutationType.getFields();
121
+ for (const [mutName, mutField] of Object.entries(mutFields)) {
122
+ const returnType = unwrapType(mutField.type);
123
+ let returnFields = "value";
124
+ if (isObjectType(returnType)) {
125
+ const scalars = getScalarFieldNames(returnType).slice(0, 6);
126
+ if (scalars.length > 0)
127
+ returnFields = scalars.join(" ");
128
+ }
129
+ const hasArgs = mutField.args.length > 0;
130
+ suggestions.push({
131
+ name: `Mutation: ${mutName}`,
132
+ query: `mutation { ${mutName}${hasArgs ? "(input: { ... })" : ""} { ${returnFields} } }`,
133
+ description: `Write operation returning ${returnFields}`,
134
+ });
135
+ }
136
+ }
137
+ return suggestions;
138
+ }
@@ -0,0 +1,39 @@
1
+ const DEFAULT_TTL_MS = 30_000;
2
+ const cache = new Map();
3
+ export function buildCacheKey(method, pathTemplate, params, body, extraHeaders) {
4
+ const parts = [method, pathTemplate];
5
+ if (params && Object.keys(params).length > 0) {
6
+ parts.push(JSON.stringify(params, Object.keys(params).sort()));
7
+ }
8
+ if (body && Object.keys(body).length > 0) {
9
+ parts.push(JSON.stringify(body, Object.keys(body).sort()));
10
+ }
11
+ if (extraHeaders && Object.keys(extraHeaders).length > 0) {
12
+ parts.push(JSON.stringify(extraHeaders, Object.keys(extraHeaders).sort()));
13
+ }
14
+ return parts.join("|");
15
+ }
16
+ export function getCached(key) {
17
+ const entry = cache.get(key);
18
+ if (!entry)
19
+ return undefined;
20
+ if (Date.now() > entry.expiresAt) {
21
+ cache.delete(key);
22
+ return undefined;
23
+ }
24
+ return entry.data;
25
+ }
26
+ export function setCache(key, data, ttlMs = DEFAULT_TTL_MS) {
27
+ cache.set(key, { data, expiresAt: Date.now() + ttlMs });
28
+ }
29
+ export function evictExpired() {
30
+ const now = Date.now();
31
+ for (const [key, entry] of cache) {
32
+ if (now > entry.expiresAt) {
33
+ cache.delete(key);
34
+ }
35
+ }
36
+ }
37
+ export function clearCache() {
38
+ cache.clear();
39
+ }
@@ -0,0 +1,80 @@
1
+ import { XMLParser } from "fast-xml-parser";
2
+ const xmlParser = new XMLParser({
3
+ ignoreAttributes: false,
4
+ attributeNamePrefix: "@_",
5
+ textNodeName: "#text",
6
+ });
7
+ export function parseResponse(contentType, body) {
8
+ const ct = (contentType ?? "").toLowerCase();
9
+ if (ct.includes("application/json") || ct.includes("+json")) {
10
+ return JSON.parse(body);
11
+ }
12
+ if (ct.includes("xml") || ct.includes("+xml")) {
13
+ return xmlParser.parse(body);
14
+ }
15
+ if (ct.includes("text/csv") || ct.includes("application/csv")) {
16
+ return parseCsv(body);
17
+ }
18
+ // Try JSON parse for responses without content-type
19
+ if (!ct || ct.includes("text/plain")) {
20
+ try {
21
+ return JSON.parse(body);
22
+ }
23
+ catch {
24
+ // Not JSON, wrap as text
25
+ }
26
+ }
27
+ return { _type: "text", content: body };
28
+ }
29
+ function parseCsv(csv) {
30
+ const lines = csv.split("\n").filter((line) => line.trim().length > 0);
31
+ if (lines.length === 0)
32
+ return [];
33
+ const headers = parseCsvLine(lines[0]);
34
+ const rows = [];
35
+ for (let i = 1; i < lines.length; i++) {
36
+ const values = parseCsvLine(lines[i]);
37
+ const row = {};
38
+ for (let j = 0; j < headers.length; j++) {
39
+ row[headers[j]] = values[j] ?? "";
40
+ }
41
+ rows.push(row);
42
+ }
43
+ return rows;
44
+ }
45
+ function parseCsvLine(line) {
46
+ const fields = [];
47
+ let current = "";
48
+ let inQuotes = false;
49
+ for (let i = 0; i < line.length; i++) {
50
+ const char = line[i];
51
+ if (inQuotes) {
52
+ if (char === '"') {
53
+ if (line[i + 1] === '"') {
54
+ current += '"';
55
+ i++;
56
+ }
57
+ else {
58
+ inQuotes = false;
59
+ }
60
+ }
61
+ else {
62
+ current += char;
63
+ }
64
+ }
65
+ else {
66
+ if (char === '"') {
67
+ inQuotes = true;
68
+ }
69
+ else if (char === ",") {
70
+ fields.push(current.trim());
71
+ current = "";
72
+ }
73
+ else {
74
+ current += char;
75
+ }
76
+ }
77
+ }
78
+ fields.push(current.trim());
79
+ return fields;
80
+ }
package/build/retry.js ADDED
@@ -0,0 +1,53 @@
1
+ const DEFAULT_OPTIONS = {
2
+ maxRetries: 3,
3
+ baseDelayMs: 1000,
4
+ maxDelayMs: 10_000,
5
+ };
6
+ const RETRYABLE_STATUSES = new Set([429, 500, 502, 503, 504]);
7
+ export class RetryableError extends Error {
8
+ status;
9
+ retryAfterMs;
10
+ constructor(message, status, retryAfterMs) {
11
+ super(message);
12
+ this.status = status;
13
+ this.retryAfterMs = retryAfterMs;
14
+ this.name = "RetryableError";
15
+ }
16
+ }
17
+ export function isRetryableStatus(status) {
18
+ return RETRYABLE_STATUSES.has(status);
19
+ }
20
+ function isRetryable(error) {
21
+ if (error instanceof RetryableError)
22
+ return true;
23
+ if (error instanceof TypeError)
24
+ return true;
25
+ return false;
26
+ }
27
+ function computeDelay(attempt, options, retryAfterMs) {
28
+ if (retryAfterMs !== undefined) {
29
+ return Math.min(retryAfterMs, options.maxDelayMs);
30
+ }
31
+ const exponential = options.baseDelayMs * Math.pow(2, attempt);
32
+ const jitter = Math.random() * options.baseDelayMs;
33
+ return Math.min(exponential + jitter, options.maxDelayMs);
34
+ }
35
+ export async function withRetry(fn, options) {
36
+ const opts = { ...DEFAULT_OPTIONS, ...options };
37
+ let lastError;
38
+ for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
39
+ try {
40
+ return await fn();
41
+ }
42
+ catch (error) {
43
+ lastError = error;
44
+ if (attempt >= opts.maxRetries || !isRetryable(error)) {
45
+ throw error;
46
+ }
47
+ const retryAfterMs = error instanceof RetryableError ? error.retryAfterMs : undefined;
48
+ const delay = computeDelay(attempt, opts, retryAfterMs);
49
+ await new Promise((resolve) => setTimeout(resolve, delay));
50
+ }
51
+ }
52
+ throw lastError;
53
+ }
package/build/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "anyapi-mcp-server",
3
+ "version": "1.1.1",
4
+ "description": "A universal MCP server that connects any REST API (via OpenAPI spec) to AI assistants, with GraphQL-style field selection and automatic schema inference.",
5
+ "type": "module",
6
+ "license": "SEE LICENSE IN LICENSE",
7
+ "keywords": [
8
+ "mcp",
9
+ "model-context-protocol",
10
+ "openapi",
11
+ "rest-api",
12
+ "ai",
13
+ "graphql",
14
+ "claude",
15
+ "cursor",
16
+ "llm"
17
+ ],
18
+ "bin": {
19
+ "anyapi-mcp": "./build/index.js"
20
+ },
21
+ "files": [
22
+ "build",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsc",
28
+ "start": "node build/index.js",
29
+ "prepublishOnly": "npm run build"
30
+ },
31
+ "engines": {
32
+ "node": ">=18"
33
+ },
34
+ "dependencies": {
35
+ "@modelcontextprotocol/sdk": "^1.12.0",
36
+ "fast-xml-parser": "^5.3.5",
37
+ "graphql": "^16.10.0",
38
+ "js-yaml": "^4.1.0",
39
+ "zod": "^3.25.0"
40
+ },
41
+ "devDependencies": {
42
+ "@types/js-yaml": "^4.0.9",
43
+ "@types/node": "^22.0.0",
44
+ "typescript": "^5.7.0"
45
+ }
46
+ }