ebay-api-mcp-server-node-local 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 +214 -0
- package/dist/constant/constants.js +48 -0
- package/dist/constant/constants.js.map +1 -0
- package/dist/helper/http-helper.js +140 -0
- package/dist/helper/http-helper.js.map +1 -0
- package/dist/helper/openapi-helper.js +114 -0
- package/dist/helper/openapi-helper.js.map +1 -0
- package/dist/helper/validation-helper.js +214 -0
- package/dist/helper/validation-helper.js.map +1 -0
- package/dist/index.js +65 -0
- package/dist/index.js.map +1 -0
- package/dist/service/openapi-service.js +196 -0
- package/dist/service/openapi-service.js.map +1 -0
- package/eslint.config.js +58 -0
- package/package.json +50 -0
- package/src/constant/constants.ts +51 -0
- package/src/helper/http-helper.ts +154 -0
- package/src/helper/openapi-helper.ts +116 -0
- package/src/helper/validation-helper.ts +289 -0
- package/src/index.ts +75 -0
- package/src/integration.test.ts +140 -0
- package/src/service/openapi-service.ts +243 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.json +115 -0
- package/tsconfig.node.json +7 -0
- package/vitest.config.ts +20 -0
- package/vitest.setup.ts +6 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constants used throughout the application
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Environment options for the eBay API
|
|
7
|
+
*/
|
|
8
|
+
export enum ApiEnvironment {
|
|
9
|
+
SANDBOX = "sandbox",
|
|
10
|
+
PRODUCTION = "production"
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Utility to find ApiEnvironment by string value
|
|
14
|
+
* Returns SANDBOX if no match is found
|
|
15
|
+
*/
|
|
16
|
+
export function findApiEnvironmentByValue(value: string): ApiEnvironment {
|
|
17
|
+
const lowercaseValue = value?.toLowerCase();
|
|
18
|
+
const matchedEnv = Object.entries(ApiEnvironment)
|
|
19
|
+
.find(([_key, val]) => typeof val === "string" && val.toLowerCase() === lowercaseValue);
|
|
20
|
+
return matchedEnv ? matchedEnv[1] as ApiEnvironment : ApiEnvironment.SANDBOX;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Recall apiDoc url by prompt
|
|
25
|
+
*/
|
|
26
|
+
export const RECALL_SPEC_BY_PROMPT_URL = "https://ebaypubapimcp3.vip.qa.ebay.com/developer-portal/api/v1/openapi-specs/search?query=%s";
|
|
27
|
+
/**
|
|
28
|
+
* url for query apiSpec with fields such as specTitle、operationId
|
|
29
|
+
*/
|
|
30
|
+
export const RECALL_SPEC_WITH_FIELD_URL = "https://ebaypubapimcp3.vip.qa.ebay.com/developer-portal/api/v1/openapi-specs/%s?operationId=%s";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Required environment variable names for the application
|
|
34
|
+
*/
|
|
35
|
+
export const REQUIRED_ENV_VARS = ["EBAY_CLIENT_TOKEN", "EBAY_API_ENV"];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* API domain name, differentiated by api environment
|
|
39
|
+
*/
|
|
40
|
+
export const DOMAIN_NAME = {
|
|
41
|
+
[ApiEnvironment.SANDBOX]: "api.sandbox.ebay.com",
|
|
42
|
+
[ApiEnvironment.PRODUCTION]: "api.ebay.com",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* List of supported calling methods, differentiated by api environment
|
|
47
|
+
*/
|
|
48
|
+
export const SUPPORTED_CALLING_METHODS = {
|
|
49
|
+
[ApiEnvironment.SANDBOX]: ["get", "put", "post", "delete", "options", "head", "patch", "trace"],
|
|
50
|
+
[ApiEnvironment.PRODUCTION]: ["get"],
|
|
51
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper functions for HTTP requests
|
|
3
|
+
*/
|
|
4
|
+
import axios from "axios";
|
|
5
|
+
import { type OpenAPIV3 } from "openapi-types";
|
|
6
|
+
import https from "https";
|
|
7
|
+
import { DOMAIN_NAME, findApiEnvironmentByValue } from "../constant/constants.js";
|
|
8
|
+
|
|
9
|
+
const USER_ENVIRONMENT = findApiEnvironmentByValue(process.env.EBAY_API_ENV || "");
|
|
10
|
+
const USER_TOKEN = process.env.EBAY_CLIENT_TOKEN || "";
|
|
11
|
+
const SCHEMA_REQUEST_BODY = "requestBody";
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Format axios error message for consistent error output
|
|
16
|
+
*/
|
|
17
|
+
export function formatAxiosError(error: unknown): string {
|
|
18
|
+
let errorMessage = `Error in invokeAPI tool: ${error instanceof Error ? error.message : String(error)}`;
|
|
19
|
+
if (axios.isAxiosError(error)) {
|
|
20
|
+
if (error?.response?.data) {
|
|
21
|
+
errorMessage = JSON.stringify(error.response.data, null, 2);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return errorMessage;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build headers from input headers and fill with default headers
|
|
29
|
+
*/
|
|
30
|
+
export function buildHeadersFromInput(inputHeaders: Record<string, string[]> | undefined): Record<string, string> {
|
|
31
|
+
const headers: Record<string, string> = {};
|
|
32
|
+
if (inputHeaders) {
|
|
33
|
+
for (const [key, value] of Object.entries(inputHeaders)) {
|
|
34
|
+
headers[key] = Array.isArray(value) ? value[0] : value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Add default headers
|
|
38
|
+
fillDefaultHeaderInfo(headers);
|
|
39
|
+
return headers;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function fillDefaultHeaderInfo(headers: Record<string, string>): void {
|
|
43
|
+
headers["Host"] = DOMAIN_NAME[USER_ENVIRONMENT];
|
|
44
|
+
headers["User-Agent"] = "EBAY-API-MCP-Tool/1.0";
|
|
45
|
+
headers["Authorization"] = `Bearer ${USER_TOKEN}`;
|
|
46
|
+
headers["Content-Type"] = headers["Content-Type"] || "application/json";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Build final URL by replacing path variables with their values
|
|
51
|
+
*/
|
|
52
|
+
export function buildFinalUrl(url: string, urlVariables: Record<string, unknown> | undefined): string {
|
|
53
|
+
if (urlVariables) {
|
|
54
|
+
for (const [key, value] of Object.entries(urlVariables)) {
|
|
55
|
+
url = url.replace(new RegExp(`%7B${key}%7D`, "g"), encodeURIComponent(String(value)));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return url;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* replace url domain name by environment
|
|
63
|
+
*/
|
|
64
|
+
export function replaceDomainNameByEnvironment(url: string): string {
|
|
65
|
+
try {
|
|
66
|
+
const urlObj = new URL(url);
|
|
67
|
+
const currentHostname = urlObj.hostname;
|
|
68
|
+
const expectedDomain = DOMAIN_NAME[USER_ENVIRONMENT];
|
|
69
|
+
if (currentHostname !== expectedDomain) {
|
|
70
|
+
urlObj.hostname = expectedDomain;
|
|
71
|
+
return urlObj.toString();
|
|
72
|
+
}
|
|
73
|
+
return url;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
// Fallback to string replacement if URL parsing fails
|
|
76
|
+
const expectedDomain = DOMAIN_NAME[USER_ENVIRONMENT];
|
|
77
|
+
for (const [env, domain] of Object.entries(DOMAIN_NAME)) {
|
|
78
|
+
if (env !== USER_ENVIRONMENT && url.includes(domain)) {
|
|
79
|
+
return url.replace(domain, expectedDomain);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return url;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build base URL from OpenAPI servers object
|
|
88
|
+
*/
|
|
89
|
+
export function buildBaseUrlFromOpenApi(openapi: OpenAPIV3.Document): string {
|
|
90
|
+
const serverObj = openapi.servers?.[0] || { url: "" };
|
|
91
|
+
let baseUrl = serverObj.url;
|
|
92
|
+
if (serverObj.variables) {
|
|
93
|
+
for (const [key, value] of Object.entries(serverObj.variables)) {
|
|
94
|
+
baseUrl = baseUrl.replace(`{${key}}`, value.default);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return replaceDomainNameByEnvironment(baseUrl);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolve path variables in a URL pattern
|
|
102
|
+
*/
|
|
103
|
+
export function resolvePath(pathPattern: string, pathVariables?: Record<string, string | number>): string {
|
|
104
|
+
if (!pathVariables) {
|
|
105
|
+
return pathPattern;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return pathPattern.replace(/{([^}]+)}/g, (match, key) => {
|
|
109
|
+
const value = pathVariables[key];
|
|
110
|
+
if (value === undefined) {
|
|
111
|
+
throw new Error(`Missing path variable: ${key}`);
|
|
112
|
+
}
|
|
113
|
+
return String(value);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Prepare request data
|
|
119
|
+
*/
|
|
120
|
+
export function prepareRequestData(
|
|
121
|
+
input: Record<string, unknown>,
|
|
122
|
+
operation: OpenAPIV3.OperationObject,
|
|
123
|
+
path: string,
|
|
124
|
+
): { resolvedPath: string; headers: Record<string, string>; params: Record<string, unknown>; data: unknown; } {
|
|
125
|
+
let resolvedPath = path;
|
|
126
|
+
const headers: Record<string, string> = {};
|
|
127
|
+
const params: Record<string, unknown> = {};
|
|
128
|
+
let data: unknown = undefined;
|
|
129
|
+
const pathParams: Record<string, string> = {};
|
|
130
|
+
|
|
131
|
+
let prop = input.properties as Record<string, unknown> || {};
|
|
132
|
+
Object.entries(prop).forEach(([key, value]) => {
|
|
133
|
+
if (key === SCHEMA_REQUEST_BODY) {
|
|
134
|
+
data = value;
|
|
135
|
+
} else {
|
|
136
|
+
const paramDef = operation.parameters?.find(p =>
|
|
137
|
+
!("$ref" in p) && p.name === key) as OpenAPIV3.ParameterObject | undefined;
|
|
138
|
+
if (paramDef) {
|
|
139
|
+
if (paramDef.in === "header") {
|
|
140
|
+
headers[key] = String(value);
|
|
141
|
+
} else if (paramDef.in === "query") {
|
|
142
|
+
params[key] = value;
|
|
143
|
+
} else if (paramDef.in === "path") {
|
|
144
|
+
pathParams[key] = String(value);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
fillDefaultHeaderInfo(headers);
|
|
150
|
+
if (Object.keys(pathParams).length > 0) {
|
|
151
|
+
resolvedPath = resolvePath(resolvedPath, pathParams);
|
|
152
|
+
}
|
|
153
|
+
return { resolvedPath, headers, params, data };
|
|
154
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAPI helper functions, including loading OpenAPI documents,
|
|
3
|
+
* parsing OpenAPI specs, building schemas, and converting to Zod schemas.
|
|
4
|
+
*/
|
|
5
|
+
import SwaggerParser from "@apidevtools/swagger-parser";
|
|
6
|
+
import { type OpenAPIV3 } from "openapi-types";
|
|
7
|
+
import * as fs from "fs";
|
|
8
|
+
import axios from "axios";
|
|
9
|
+
import * as yaml from "js-yaml";
|
|
10
|
+
import util from "util";
|
|
11
|
+
import { z, type ZodTypeAny } from "zod";
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
const SCHEMA_REQUEST_BODY = "requestBody";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get OpenAPI docs from user config file, which contains urls or paths of OpenAPI specs.
|
|
18
|
+
*/
|
|
19
|
+
export async function getOpenApiDocumentsFromConfigFile(): Promise<OpenAPIV3.Document[]> {
|
|
20
|
+
const docs: OpenAPIV3.Document[] = [];
|
|
21
|
+
const urlFile = process.env.EBAY_API_DOC_URL_FILE;
|
|
22
|
+
let urls: string[] = [];
|
|
23
|
+
if (urlFile && fs.existsSync(urlFile)) {
|
|
24
|
+
// url or path each line in the file
|
|
25
|
+
urls = fs.readFileSync(urlFile, "utf-8").split(/\r?\n/)
|
|
26
|
+
.map(line => line.trim())
|
|
27
|
+
.filter(line => line.length > 0 && !line.startsWith("#"));
|
|
28
|
+
}
|
|
29
|
+
console.error("Loading OpenAPI specifications from:", urls);
|
|
30
|
+
// parse opebapi doc from url/path
|
|
31
|
+
for (const specPath of urls) {
|
|
32
|
+
try {
|
|
33
|
+
const doc = await SwaggerParser.dereference(specPath) as OpenAPIV3.Document;
|
|
34
|
+
docs.push(doc);
|
|
35
|
+
} catch (e) {
|
|
36
|
+
console.error(`getOpenApiDocumentsFromConfigFile#[Failed to load OpenAPI doc from the specPath : ${specPath}]`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return docs;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Helper: remove ignored keys recursively
|
|
43
|
+
export function readSchema2Map(obj: unknown): unknown {
|
|
44
|
+
const SCHEMA_IGNORE_KEYS = ["style", "explode", "exampleSetFlag", "types", "in", "required"];
|
|
45
|
+
if (Array.isArray(obj)) {
|
|
46
|
+
return obj.map(readSchema2Map);
|
|
47
|
+
} else if (obj && typeof obj === "object") {
|
|
48
|
+
const out: Record<string, unknown> = {};
|
|
49
|
+
for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
|
|
50
|
+
if (!SCHEMA_IGNORE_KEYS.includes(k)) {
|
|
51
|
+
out[k] = readSchema2Map(v);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
return obj;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Parse OpenAPI document from string (supports both JSON and YAML)
|
|
61
|
+
*/
|
|
62
|
+
export async function parseOpenApiDoc(specTitle: string, operationId : string, specUrl: string): Promise<OpenAPIV3.Document> {
|
|
63
|
+
const url = util.format(specUrl, specTitle, operationId);
|
|
64
|
+
const apiSpecRes = await axios.get<string>(url, {
|
|
65
|
+
httpsAgent: new (await import("https")).Agent({
|
|
66
|
+
rejectUnauthorized: false,
|
|
67
|
+
})});
|
|
68
|
+
const docString = apiSpecRes.data;
|
|
69
|
+
try {
|
|
70
|
+
// Try parsing as JSON first
|
|
71
|
+
return JSON.parse(docString) as OpenAPIV3.Document;
|
|
72
|
+
} catch (jsonError) {
|
|
73
|
+
try {
|
|
74
|
+
// If JSON fails, try parsing as YAML
|
|
75
|
+
return yaml.load(docString) as OpenAPIV3.Document;
|
|
76
|
+
} catch (yamlError) {
|
|
77
|
+
const _jsonMsg = jsonError instanceof Error ? jsonError.message : String(jsonError);
|
|
78
|
+
const _yamlMsg = yamlError instanceof Error ? yamlError.message : String(yamlError);
|
|
79
|
+
console.error("failed to parse OpenAPI document !!!");
|
|
80
|
+
return {} as OpenAPIV3.Document;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Build schema for an operation's input parameters
|
|
87
|
+
*/
|
|
88
|
+
export function buildOperationSchema(operation: OpenAPIV3.OperationObject): { properties: Record<string, unknown> } {
|
|
89
|
+
const properties: Record<string, unknown> = {};
|
|
90
|
+
// handle request param
|
|
91
|
+
(operation.parameters || []).forEach(param => {
|
|
92
|
+
if ("$ref" in param) {return;}
|
|
93
|
+
const paramSchema = readSchema2Map(param);
|
|
94
|
+
properties[param.name] = paramSchema;
|
|
95
|
+
});
|
|
96
|
+
// handle request body
|
|
97
|
+
if (operation.requestBody && "content" in operation.requestBody &&
|
|
98
|
+
operation.requestBody.content?.["application/json"]?.schema) {
|
|
99
|
+
const requestBodySchema = readSchema2Map(operation.requestBody.content["application/json"].schema);
|
|
100
|
+
properties[SCHEMA_REQUEST_BODY] = requestBodySchema;
|
|
101
|
+
}
|
|
102
|
+
return { properties };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Build Zod validation schema
|
|
107
|
+
*/
|
|
108
|
+
export function buildZodSchema(properties: Record<string, unknown>): Record<string, ZodTypeAny> {
|
|
109
|
+
const zodProperties: Record<string, ZodTypeAny> = {};
|
|
110
|
+
|
|
111
|
+
Object.keys(properties).forEach(key => {
|
|
112
|
+
zodProperties[key] = z.any();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return zodProperties;
|
|
116
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation helper functions for OpenAPI validation
|
|
3
|
+
*/
|
|
4
|
+
import { type OpenAPIV3 } from "openapi-types";
|
|
5
|
+
import AjvLib from "ajv";
|
|
6
|
+
const Ajv = AjvLib.default || AjvLib;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Validate request parameters against OpenAPI specification
|
|
10
|
+
*/
|
|
11
|
+
export function validateRequestParameters(
|
|
12
|
+
url: string,
|
|
13
|
+
openApiDoc: OpenAPIV3.Document,
|
|
14
|
+
method: string,
|
|
15
|
+
input: {
|
|
16
|
+
urlVariables?: Record<string, unknown>;
|
|
17
|
+
urlQueryParams?: Record<string, unknown>;
|
|
18
|
+
headers?: Record<string, string>;
|
|
19
|
+
requestBody?: Record<string, unknown>;
|
|
20
|
+
},
|
|
21
|
+
): { isValid: boolean; errors: string[] } {
|
|
22
|
+
const errors: string[] = [];
|
|
23
|
+
|
|
24
|
+
// validate path
|
|
25
|
+
const {pathValidateRes, apiPath, specPath, specPathItem} = validatePath(openApiDoc, url);
|
|
26
|
+
if (!pathValidateRes || !specPathItem) {
|
|
27
|
+
errors.push(`API path ${apiPath} is not valid or not found in OpenAPI specification`);
|
|
28
|
+
return { isValid: false, errors };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check if the method exists for the path
|
|
32
|
+
const operation = specPathItem[method.toLowerCase() as keyof typeof specPathItem] as OpenAPIV3.OperationObject;
|
|
33
|
+
if (!operation) {
|
|
34
|
+
errors.push(`Method ${method} not found for path ${specPath} in OpenAPI specification`);
|
|
35
|
+
return { isValid: false, errors };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Initialize AJV for schema validation
|
|
39
|
+
const _ajv = new Ajv({ allErrors: true });
|
|
40
|
+
|
|
41
|
+
validateUrlPathParam(input.urlVariables, operation.parameters, errors);
|
|
42
|
+
|
|
43
|
+
validateUrlQueryParam(input.urlQueryParams, operation.parameters, errors);
|
|
44
|
+
|
|
45
|
+
validateUrlHeaders(input.headers, operation.parameters, errors);
|
|
46
|
+
|
|
47
|
+
validateUrlRequestBody(input.requestBody, operation.requestBody, errors);
|
|
48
|
+
|
|
49
|
+
return { isValid: errors.length === 0, errors };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Validate the API path against the OpenAPI document
|
|
54
|
+
*/
|
|
55
|
+
export function validatePath(openApiDoc: OpenAPIV3.Document, inputUrl: string): {
|
|
56
|
+
pathValidateRes : boolean,
|
|
57
|
+
apiPath : string,
|
|
58
|
+
specPath : string,
|
|
59
|
+
specPathItem?: OpenAPIV3.PathItemObject
|
|
60
|
+
} {
|
|
61
|
+
// Extract the path from the URL (remove base URL part)
|
|
62
|
+
const apiPath : string = parseApiPathFromUrl(inputUrl);
|
|
63
|
+
if (!apiPath) {
|
|
64
|
+
console.error(`input url ${inputUrl} is not valid, please check it.`);
|
|
65
|
+
return {pathValidateRes : false, apiPath:"", specPath: "", specPathItem: undefined};
|
|
66
|
+
}
|
|
67
|
+
// bathPath validation
|
|
68
|
+
const serverObj = openApiDoc.servers?.[0] || { url: "" };
|
|
69
|
+
let basePath : string = "";
|
|
70
|
+
if (serverObj?.variables) {
|
|
71
|
+
for (const [key, value] of Object.entries(serverObj.variables)) {
|
|
72
|
+
if (key === "basePath") {
|
|
73
|
+
basePath = value.default;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const basePathRegex = new RegExp(`${basePath.replace(/\{[^}]+\}/g, "[^/]+") }$`);
|
|
78
|
+
if (basePath && !basePathRegex.test(apiPath)) {
|
|
79
|
+
console.error(`API path ${apiPath} does not match the base path ${basePath} in OpenAPI specification.`);
|
|
80
|
+
return {pathValidateRes : false, apiPath, specPath: "", specPathItem: undefined};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// specPath validation
|
|
84
|
+
for (const specPath of Object.keys(openApiDoc.paths || {})) {
|
|
85
|
+
const specPathRegex = new RegExp(`${specPath.replace(/\{[^}]+\}/g, "[^/]+") }$`);
|
|
86
|
+
if (specPathRegex.test(apiPath) && openApiDoc.paths?.[specPath]) {
|
|
87
|
+
const specPathItem = openApiDoc.paths[specPath];
|
|
88
|
+
return { pathValidateRes: true, apiPath, specPath, specPathItem};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {pathValidateRes : false, apiPath, specPath: "", specPathItem: undefined};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* parse path from the given url by llm
|
|
97
|
+
*/
|
|
98
|
+
export function parseApiPathFromUrl(inputUrl: string): string {
|
|
99
|
+
try {
|
|
100
|
+
const urlObj = new URL(inputUrl);
|
|
101
|
+
return decodeURIComponent(urlObj.pathname); // Decode URL path
|
|
102
|
+
} catch (urlError) {
|
|
103
|
+
// If URL parsing fails, try to extract path manually
|
|
104
|
+
const pathMatch = inputUrl.match(/https?:\/\/[^/]+(.*)$/);
|
|
105
|
+
if (pathMatch) {
|
|
106
|
+
return pathMatch[1];
|
|
107
|
+
}
|
|
108
|
+
console.error(`Failed to parse API path from URL: ${inputUrl}`);
|
|
109
|
+
return "";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Validate URL path parameters against OpenAPI specification
|
|
115
|
+
*/
|
|
116
|
+
export function validateUrlPathParam(
|
|
117
|
+
urlVariables: Record<string, unknown> | undefined,
|
|
118
|
+
parameters: (OpenAPIV3.ReferenceObject | OpenAPIV3.ParameterObject)[] | undefined,
|
|
119
|
+
errors: string[],
|
|
120
|
+
): void {
|
|
121
|
+
// Initialize AJV for schema validation
|
|
122
|
+
const ajv = new Ajv({ allErrors: true });
|
|
123
|
+
if (urlVariables) {
|
|
124
|
+
const pathParams = (parameters || []).filter(
|
|
125
|
+
(param): param is OpenAPIV3.ParameterObject =>
|
|
126
|
+
!("$ref" in param) && param.in === "path",
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
for (const param of pathParams) {
|
|
130
|
+
const value = urlVariables[param.name];
|
|
131
|
+
|
|
132
|
+
if (param.required && (value === undefined || value === null)) {
|
|
133
|
+
errors.push(`Missing required path parameter: ${param.name}`);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (value !== undefined && param.schema) {
|
|
138
|
+
const validate = ajv.compile(param.schema);
|
|
139
|
+
if (!validate(value)) {
|
|
140
|
+
errors.push(`Invalid path parameter ${param.name}: ${ajv.errorsText(validate.errors)}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Check for extra path parameters not defined in spec
|
|
146
|
+
const definedPathParams = pathParams.map(p => p.name);
|
|
147
|
+
const extraParams = Object.keys(urlVariables).filter(key => !definedPathParams.includes(key));
|
|
148
|
+
if (extraParams.length > 0) {
|
|
149
|
+
errors.push(`Unknown path parameters: ${extraParams.join(", ")}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check for required path parameters that might be missing from urlVariables
|
|
153
|
+
const requiredPathParams = (parameters || [])
|
|
154
|
+
.filter((param): param is OpenAPIV3.ParameterObject =>
|
|
155
|
+
!("$ref" in param) && param.in === "path" && (param.required === true),
|
|
156
|
+
)
|
|
157
|
+
.map(param => param.name);
|
|
158
|
+
const providedPathParams = Object.keys(urlVariables || {});
|
|
159
|
+
const missingPathParams = requiredPathParams.filter(param => !providedPathParams.includes(param));
|
|
160
|
+
if (missingPathParams.length > 0) {
|
|
161
|
+
errors.push(`Missing required path parameters: ${missingPathParams.join(", ")}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Validate URL query parameters against OpenAPI specification
|
|
168
|
+
*/
|
|
169
|
+
export function validateUrlQueryParam(
|
|
170
|
+
urlQueryParams: Record<string, unknown> | undefined,
|
|
171
|
+
parameters: (OpenAPIV3.ReferenceObject | OpenAPIV3.ParameterObject)[] | undefined,
|
|
172
|
+
errors: string[],
|
|
173
|
+
): void {
|
|
174
|
+
const ajv = new Ajv({ allErrors: true });
|
|
175
|
+
if (urlQueryParams) {
|
|
176
|
+
const queryParams = (parameters || []).filter(
|
|
177
|
+
(param): param is OpenAPIV3.ParameterObject =>
|
|
178
|
+
!("$ref" in param) && param.in === "query",
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
for (const param of queryParams) {
|
|
182
|
+
const value = urlQueryParams[param.name];
|
|
183
|
+
|
|
184
|
+
if (param.required && (value === undefined || value === null || value === "")) {
|
|
185
|
+
errors.push(`Missing required query parameter: ${param.name}`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (value !== undefined && param.schema) {
|
|
190
|
+
const validate = ajv.compile(param.schema);
|
|
191
|
+
// Convert string values to appropriate types for validation
|
|
192
|
+
let validationValue = value;
|
|
193
|
+
if (typeof value === "string" && param.schema && !("$ref" in param.schema) && (param.schema).type) {
|
|
194
|
+
const schemaObj = param.schema;
|
|
195
|
+
switch (schemaObj.type) {
|
|
196
|
+
case "integer":
|
|
197
|
+
case "number":
|
|
198
|
+
validationValue = Number(value);
|
|
199
|
+
break;
|
|
200
|
+
case "boolean":
|
|
201
|
+
validationValue = value.toLowerCase() === "true";
|
|
202
|
+
break;
|
|
203
|
+
case "array":
|
|
204
|
+
// Handle comma-separated values for arrays
|
|
205
|
+
if (param.style === "form" && !param.explode) {
|
|
206
|
+
validationValue = value.split(",");
|
|
207
|
+
}
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!validate(validationValue)) {
|
|
213
|
+
errors.push(`Invalid query parameter ${param.name}: ${ajv.errorsText(validate.errors)}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Validate URL headers against OpenAPI specification
|
|
222
|
+
*/
|
|
223
|
+
export function validateUrlHeaders(
|
|
224
|
+
headers: Record<string, string> | undefined,
|
|
225
|
+
parameters: (OpenAPIV3.ReferenceObject | OpenAPIV3.ParameterObject)[] | undefined,
|
|
226
|
+
errors: string[],
|
|
227
|
+
): void {
|
|
228
|
+
const ajv = new Ajv({ allErrors: true });
|
|
229
|
+
if (headers) {
|
|
230
|
+
const headerParams = (parameters || []).filter(
|
|
231
|
+
(param): param is OpenAPIV3.ParameterObject =>
|
|
232
|
+
!("$ref" in param) && param.in === "header",
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
for (const param of headerParams) {
|
|
236
|
+
const headerName = param.name.toLowerCase();
|
|
237
|
+
const value = headers[headerName] || headers[param.name];
|
|
238
|
+
|
|
239
|
+
if (param.required && (value === undefined || value === null || value === "")) {
|
|
240
|
+
errors.push(`Missing required header parameter: ${param.name}`);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (value !== undefined && param.schema) {
|
|
245
|
+
const validate = ajv.compile(param.schema);
|
|
246
|
+
if (!validate(value)) {
|
|
247
|
+
errors.push(`Invalid header parameter ${param.name}: ${ajv.errorsText(validate.errors)}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Validate URL request body against OpenAPI specification
|
|
256
|
+
*/
|
|
257
|
+
export function validateUrlRequestBody(
|
|
258
|
+
inputBody: Record<string, unknown> | undefined,
|
|
259
|
+
operationBody: OpenAPIV3.ReferenceObject | OpenAPIV3.RequestBodyObject | undefined,
|
|
260
|
+
errors: string[],
|
|
261
|
+
): void {
|
|
262
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
263
|
+
if (operationBody && !("$ref" in operationBody)) {
|
|
264
|
+
const requestBody = operationBody;
|
|
265
|
+
const isRequired = requestBody.required || false;
|
|
266
|
+
|
|
267
|
+
if (isRequired && (!inputBody || Object.keys(inputBody).length === 0)) {
|
|
268
|
+
errors.push("Missing required request body");
|
|
269
|
+
} else if (inputBody && Object.keys(inputBody).length > 0) {
|
|
270
|
+
// Find the appropriate content type schema
|
|
271
|
+
const contentTypes = ["application/json", "application/x-www-form-urlencoded", "multipart/form-data"];
|
|
272
|
+
let schema: OpenAPIV3.SchemaObject | undefined;
|
|
273
|
+
|
|
274
|
+
for (const contentType of contentTypes) {
|
|
275
|
+
if (requestBody.content?.[contentType]?.schema) {
|
|
276
|
+
schema = requestBody.content[contentType].schema as OpenAPIV3.SchemaObject;
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (schema) {
|
|
282
|
+
const validate = ajv.compile(schema);
|
|
283
|
+
if (!validate(inputBody)) {
|
|
284
|
+
errors.push(`Invalid request body: ${ajv.errorsText(validate.errors)}`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Load environment variables from .env file
|
|
2
|
+
import * as dotenv from "dotenv";
|
|
3
|
+
dotenv.config();
|
|
4
|
+
|
|
5
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
import { registerOpenApiTools } from "./service/openapi-service.js";
|
|
8
|
+
import * as constants from "./constant/constants.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check if required environment variables are set for eBay API authentication
|
|
12
|
+
*/
|
|
13
|
+
function checkEnvironmentVariables(): void {
|
|
14
|
+
|
|
15
|
+
// environment vals check
|
|
16
|
+
const missingVars = constants.REQUIRED_ENV_VARS.filter(varName => !process.env[varName]);
|
|
17
|
+
|
|
18
|
+
if (missingVars.length > 0) {
|
|
19
|
+
console.error(`Missing required environment variables: ${missingVars.join(", ")}`);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (process.env.EBAY_ENVIRONMENT &&
|
|
24
|
+
!["sandbox", "production"].includes(process.env.EBAY_ENVIRONMENT.toLowerCase())) {
|
|
25
|
+
console.warn("Warning: EBAY_ENVIRONMENT must be either \"sandbox\" or \"production\"");
|
|
26
|
+
console.warn(`Current value "${process.env.EBAY_ENVIRONMENT}" is invalid, defaulting to "production"`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Main function to initialize and run the eBay API MCP Server
|
|
32
|
+
* This server exposes eBay API endpoints as MCP tools for access via AI models
|
|
33
|
+
*/
|
|
34
|
+
async function main(): Promise<void> {
|
|
35
|
+
console.error("Starting eBay API MCP Server...");
|
|
36
|
+
// Check for required environment variables
|
|
37
|
+
checkEnvironmentVariables();
|
|
38
|
+
const server = initServer();
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
// Register the OpenAPI tools with the server
|
|
42
|
+
await registerOpenApiTools(server);
|
|
43
|
+
console.error("Successfully registered OpenAPI tools");
|
|
44
|
+
|
|
45
|
+
// Create and connect server transport
|
|
46
|
+
const transport = new StdioServerTransport();
|
|
47
|
+
await server.connect(transport);
|
|
48
|
+
console.error("eBay API MCP Server running on stdio transport");
|
|
49
|
+
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error("Error starting MCP server:", error instanceof Error ? error.message : String(error));
|
|
52
|
+
console.error("Stack trace:", error instanceof Error ? error.stack : "No stack trace available");
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Run the server
|
|
58
|
+
main().catch((error) => {
|
|
59
|
+
console.error("Fatal error:", error instanceof Error ? error.message : String(error));
|
|
60
|
+
console.error("Stack trace:", error instanceof Error ? error.stack : "No stack trace available");
|
|
61
|
+
process.exit(1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
// Create MCP server instance
|
|
66
|
+
function initServer(): McpServer {
|
|
67
|
+
return new McpServer({
|
|
68
|
+
name: "ebay-api-mcp-server",
|
|
69
|
+
version: "1.0.0",
|
|
70
|
+
capabilities: {
|
|
71
|
+
resources: {},
|
|
72
|
+
tools: {},
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|