@koseha/api-mcp 0.0.9 → 0.0.11

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 CHANGED
@@ -45,7 +45,10 @@ Cursor 설정 파일에 추가:
45
45
  "mcpServers": {
46
46
  "api-mcp": {
47
47
  "command": "node",
48
- "args": ["./node_modules/@koseha/api-mcp/dist/index.js"]
48
+ "args": ["./node_modules/@koseha/api-mcp/dist/index.js"],
49
+ "env": {
50
+ "OPENAPI_URL": <your swagger.json url ex.'https://petstore3.swagger.io/api/v3/openapi.json'>
51
+ }
49
52
  }
50
53
  }
51
54
  }
@@ -75,7 +78,10 @@ Claude Desktop 설정 파일에 추가:
75
78
  "mcpServers": {
76
79
  "api-mcp": {
77
80
  "command": "npx",
78
- "args": ["-y", "@koseha/api-mcp"]
81
+ "args": ["-y", "@koseha/api-mcp"],
82
+ "env": {
83
+ "OPENAPI_URL": "https://api.example.com/openapi.json"
84
+ }
79
85
  }
80
86
  }
81
87
  }
@@ -88,7 +94,10 @@ Claude Desktop 설정 파일에 추가:
88
94
  "mcpServers": {
89
95
  "api-mcp": {
90
96
  "command": "node",
91
- "args": ["/path/to/api-mcp/dist/index.js"]
97
+ "args": ["/path/to/api-mcp/dist/index.js"],
98
+ "env": {
99
+ "OPENAPI_URL": "https://api.example.com/openapi.json"
100
+ }
92
101
  }
93
102
  }
94
103
  }
@@ -203,8 +212,11 @@ api-mcp/
203
212
  │ ├── resources/ # 리소스 모듈
204
213
  │ │ ├── index.ts
205
214
  │ │ └── greeting.resource.ts
206
- │ └── swagger/ # Swagger 로더
207
- └── swaggerLoader.ts
215
+ │ └── swagger/ # Swagger 로더 및 변환기
216
+ ├── swaggerLoader.ts
217
+ │ ├── apiTransformer.ts
218
+ │ ├── schemaResolver.ts
219
+ │ └── types.ts
208
220
  ├── dist/ # 빌드된 파일
209
221
  ├── openapi.json # OpenAPI 스펙 파일
210
222
  ├── package.json
@@ -212,9 +224,15 @@ api-mcp/
212
224
  └── README.md
213
225
  ```
214
226
 
215
- ## OpenAPI 스펙 파일
227
+ ## OpenAPI 스펙 설정
216
228
 
217
- 프로젝트 루트의 `openapi.json` 파일을 수정하여 사용할 API 스펙을 설정할 있습니다. 기본적으로 5분간 캐시되며, 파일 변경 자동으로 다시 로드됩니다.
229
+ `OPENAPI_URL` 환경 변수를 통해 OpenAPI 문서의 URL을 설정해야 합니다. 서버 시작 환경 변수가 설정되지 않으면 서버가 시작되지 않습니다.
230
+
231
+ 예시:
232
+
233
+ ```bash
234
+ export OPENAPI_URL=https://api.example.com/openapi.json
235
+ ```
218
236
 
219
237
  ## 의존성
220
238
 
@@ -231,13 +249,14 @@ api-mcp/
231
249
 
232
250
  ## 버전
233
251
 
234
- 현재 버전: **0.0.7**
252
+ 현재 버전: **0.0.10**
235
253
 
236
254
  ## History
237
255
 
238
- | 버전 | 날짜 | 변경사항 |
239
- | ----- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
240
- | 0.0.7 | KST 2025.12.31 | - tool 요청 openapi.json 문서의 특정 부분만 추출하여 사용하도록 변경 <br/> >> 전체 openapi.json 문서 내용이 사용되지 않도록 함. token 절약 |
256
+ | 버전 | 날짜 | 변경사항 |
257
+ | ------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
258
+ | 0.0.10 | KST 2025.12.31 | - OpenAPI 문서를 HTTP 요청으로 로드하도록 변경 <br/> - OPENAPI_URL 환경 변수 필수 검증 추가 <br/> - API 상세 조회 로직 리팩토링 (apiTransformer 모듈화) |
259
+ | 0.0.7 | KST 2025.12.31 | - tool 요청 시 openapi.json 문서의 특정 부분만 추출하여 사용하도록 변경 <br/> >> 전체 openapi.json 문서 내용이 사용되지 않도록 함. token 절약 |
241
260
 
242
261
  ## 라이선스
243
262
 
package/dist/index.js CHANGED
@@ -6,6 +6,14 @@ import { createServer } from "./server.js";
6
6
  import { registerTools } from "./tools/index.js";
7
7
  import { registerResources } from "./resources/index.js";
8
8
  async function main() {
9
+ // OPENAPI_URL 환경 변수 검증
10
+ if (!process.env.OPENAPI_URL) {
11
+ const error = new Error("OPENAPI_URL 환경 변수가 설정되지 않았습니다. 서버를 시작할 수 없습니다.\n" +
12
+ "환경 변수를 설정한 후 다시 시도해주세요.\n" +
13
+ "예: export OPENAPI_URL=https://api.example.com/openapi.json");
14
+ process.stderr.write(error.message + "\n");
15
+ process.exit(1);
16
+ }
9
17
  const server = createServer();
10
18
  registerTools(server);
11
19
  registerResources(server);
@@ -0,0 +1,96 @@
1
+ /**
2
+ * OpenAPI Operation을 변환하는 유틸리티
3
+ */
4
+ import { resolveSchema } from "./schemaResolver.js";
5
+ /**
6
+ * 스키마를 간소화된 형태로 변환
7
+ */
8
+ function transformSchema(schema) {
9
+ if (!schema)
10
+ return null;
11
+ return {
12
+ type: schema.type || null,
13
+ format: schema.format || null,
14
+ enum: schema.enum || null,
15
+ properties: schema.properties || null,
16
+ items: schema.items || null,
17
+ required: schema.required || null,
18
+ };
19
+ }
20
+ /**
21
+ * Parameters를 변환
22
+ */
23
+ function transformParameters(parameters, swagger) {
24
+ if (!parameters || !Array.isArray(parameters)) {
25
+ return [];
26
+ }
27
+ return parameters.map((param) => {
28
+ let resolvedSchema = null;
29
+ if (param.schema) {
30
+ resolvedSchema = resolveSchema(param.schema, swagger);
31
+ }
32
+ return {
33
+ name: param.name,
34
+ in: param.in,
35
+ required: param.required || false,
36
+ description: param.description || null,
37
+ schema: transformSchema(resolvedSchema),
38
+ };
39
+ });
40
+ }
41
+ /**
42
+ * RequestBody를 변환
43
+ */
44
+ function transformRequestBody(requestBody, swagger) {
45
+ if (!requestBody?.content) {
46
+ return null;
47
+ }
48
+ const jsonContent = requestBody.content["application/json"];
49
+ if (!jsonContent?.schema) {
50
+ return null;
51
+ }
52
+ const resolvedSchema = resolveSchema(jsonContent.schema, swagger);
53
+ return {
54
+ required: resolvedSchema.required || [],
55
+ type: resolvedSchema.type || "object",
56
+ properties: resolvedSchema.properties || {},
57
+ description: requestBody.description || null,
58
+ };
59
+ }
60
+ /**
61
+ * Response를 변환 (200 응답 우선)
62
+ */
63
+ function transformResponse(responses, swagger) {
64
+ if (!responses?.["200"]?.content?.["application/json"]?.schema) {
65
+ return null;
66
+ }
67
+ const schema = responses["200"].content["application/json"].schema;
68
+ const resolvedSchema = resolveSchema(schema, swagger);
69
+ return {
70
+ type: resolvedSchema.type || "object",
71
+ properties: resolvedSchema.properties || undefined,
72
+ items: resolvedSchema.items || undefined,
73
+ };
74
+ }
75
+ /**
76
+ * OpenAPI Operation을 API 상세 정보로 변환
77
+ */
78
+ export function transformApiDetail(operation, swagger) {
79
+ const result = {};
80
+ // Parameters 변환
81
+ const parameters = transformParameters(operation.parameters, swagger);
82
+ if (parameters.length > 0) {
83
+ result.parameters = parameters;
84
+ }
85
+ // RequestBody 변환
86
+ const requestBody = transformRequestBody(operation.requestBody, swagger);
87
+ if (requestBody) {
88
+ result.requestBody = requestBody;
89
+ }
90
+ // Response 변환
91
+ const responses = transformResponse(operation.responses, swagger);
92
+ if (responses) {
93
+ result.responses = responses;
94
+ }
95
+ return result;
96
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * OpenAPI $ref 참조 해결 유틸리티
3
+ */
4
+ /**
5
+ * $ref 경로를 실제 객체 경로로 변환
6
+ * @example "#/components/schemas/Pet" -> swagger.components.schemas.Pet
7
+ */
8
+ function resolveRefPath(ref, swagger) {
9
+ if (!ref.startsWith("#/")) {
10
+ return null; // 외부 참조는 지원하지 않음
11
+ }
12
+ const path = ref.substring(2).split("/"); // "#/components/schemas/Pet" -> ["components", "schemas", "Pet"]
13
+ let result = swagger;
14
+ for (const key of path) {
15
+ if (result && typeof result === "object" && key in result) {
16
+ result = result[key];
17
+ }
18
+ else {
19
+ return null;
20
+ }
21
+ }
22
+ return result;
23
+ }
24
+ /**
25
+ * 스키마 객체의 모든 $ref를 재귀적으로 해결
26
+ * @param schema - 해결할 스키마 객체
27
+ * @param swagger - 전체 OpenAPI 스펙
28
+ * @param visited - 순환 참조 방지를 위한 방문한 $ref 경로 Set
29
+ */
30
+ export function resolveSchema(schema, swagger, visited = new Set()) {
31
+ if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
32
+ return schema;
33
+ }
34
+ // $ref가 있으면 해결
35
+ if (schema.$ref) {
36
+ const refPath = schema.$ref;
37
+ // 순환 참조 방지
38
+ if (visited.has(refPath)) {
39
+ return { $ref: refPath, _circular: true };
40
+ }
41
+ visited.add(refPath);
42
+ const resolved = resolveRefPath(refPath, swagger);
43
+ if (resolved) {
44
+ // 해결된 스키마를 재귀적으로 처리 (중첩된 $ref도 해결)
45
+ const resolvedSchema = resolveSchema(resolved, swagger, visited);
46
+ visited.delete(refPath);
47
+ return resolvedSchema;
48
+ }
49
+ visited.delete(refPath);
50
+ return schema;
51
+ }
52
+ // 배열인 경우 items 처리
53
+ if (schema.type === "array" && schema.items) {
54
+ return {
55
+ ...schema,
56
+ items: resolveSchema(schema.items, swagger, visited),
57
+ };
58
+ }
59
+ // 객체인 경우 properties와 allOf, anyOf, oneOf 처리
60
+ if (schema.type === "object" ||
61
+ schema.properties ||
62
+ schema.allOf ||
63
+ schema.anyOf ||
64
+ schema.oneOf) {
65
+ const resolved = { ...schema };
66
+ if (schema.properties) {
67
+ resolved.properties = {};
68
+ for (const [key, value] of Object.entries(schema.properties)) {
69
+ resolved.properties[key] = resolveSchema(value, swagger, visited);
70
+ }
71
+ }
72
+ if (schema.allOf) {
73
+ resolved.allOf = schema.allOf.map((item) => resolveSchema(item, swagger, visited));
74
+ }
75
+ if (schema.anyOf) {
76
+ resolved.anyOf = schema.anyOf.map((item) => resolveSchema(item, swagger, visited));
77
+ }
78
+ if (schema.oneOf) {
79
+ resolved.oneOf = schema.oneOf.map((item) => resolveSchema(item, swagger, visited));
80
+ }
81
+ return resolved;
82
+ }
83
+ return schema;
84
+ }
@@ -1,11 +1,9 @@
1
1
  /**
2
2
  * openapi.json HTTP 요청으로 읽기 + TTL 캐시
3
3
  */
4
- const DEFAULT_OPENAPI_URL = "https://petstore3.swagger.io/api/v3/openapi.json";
4
+ const OPENAPI_URL = process.env.OPENAPI_URL;
5
5
  export async function loadSwagger() {
6
- const openapiUrl = process.env.OPENAPI_URL ||
7
- process.env.OPENAPI_JSON_URL ||
8
- DEFAULT_OPENAPI_URL;
6
+ const openapiUrl = OPENAPI_URL;
9
7
  try {
10
8
  const response = await fetch(openapiUrl);
11
9
  if (!response.ok) {
@@ -0,0 +1,4 @@
1
+ /**
2
+ * OpenAPI 스펙 관련 타입 정의
3
+ */
4
+ export {};
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { z } from "zod";
5
5
  import { loadSwagger } from "../swagger/swaggerLoader.js";
6
+ import { transformApiDetail } from "../swagger/apiTransformer.js";
6
7
  export function registerGetApiDetail(server) {
7
8
  server.registerTool("getApiDetail", {
8
9
  title: "API 상세 조회",
@@ -20,55 +21,13 @@ export function registerGetApiDetail(server) {
20
21
  };
21
22
  }
22
23
  const swagger = await loadSwagger();
23
- const api = swagger.paths?.[requestUrl]?.[httpMethod];
24
- if (!api) {
24
+ const operation = swagger.paths?.[requestUrl]?.[httpMethod];
25
+ if (!operation) {
25
26
  return {
26
27
  content: [{ type: "text", text: "해당 API를 찾을 수 없습니다." }],
27
28
  };
28
29
  }
29
- // parameters 변환
30
- const parameters = (api.parameters || []).map((param) => ({
31
- name: param.name,
32
- in: param.in,
33
- required: param.required || false,
34
- schema: {
35
- type: param.schema?.type || null,
36
- format: param.schema?.format || null,
37
- enum: param.schema?.enum || null,
38
- },
39
- }));
40
- // requestBody 변환
41
- let requestBody = null;
42
- if (api.requestBody?.content) {
43
- const jsonContent = api.requestBody.content["application/json"];
44
- if (jsonContent?.schema) {
45
- const schema = jsonContent.schema;
46
- requestBody = {
47
- required: schema.required || [],
48
- type: schema.type || "object",
49
- properties: schema.properties || {},
50
- };
51
- }
52
- }
53
- // responses 변환 (200 응답의 schema 추출)
54
- let responses = null;
55
- if (api.responses?.["200"]?.content?.["application/json"]?.schema) {
56
- const schema = api.responses["200"].content["application/json"].schema;
57
- responses = {
58
- type: schema.type || "object",
59
- properties: schema.properties || {},
60
- };
61
- }
62
- const result = {};
63
- if (parameters.length > 0) {
64
- result.parameters = parameters;
65
- }
66
- if (requestBody) {
67
- result.requestBody = requestBody;
68
- }
69
- if (responses) {
70
- result.responses = responses;
71
- }
30
+ const result = transformApiDetail(operation, swagger);
72
31
  return {
73
32
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
74
33
  };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Tool: 서비스 목록 조회
3
+ */
4
+ import { loadSwagger } from "../swagger/swaggerLoader.js";
5
+ /**
6
+ * Swagger 문서에서 모든 고유한 tags 추출
7
+ */
8
+ function extractApiGroups(swagger) {
9
+ const tags = new Set();
10
+ // tags 배열에서 추출
11
+ if (Array.isArray(swagger.tags)) {
12
+ swagger.tags.forEach((tag) => {
13
+ if (typeof tag === "string") {
14
+ tags.add(tag);
15
+ }
16
+ else if (tag?.name) {
17
+ tags.add(tag.name);
18
+ }
19
+ });
20
+ }
21
+ // paths에서 tags 추출
22
+ if (swagger.paths && typeof swagger.paths === "object") {
23
+ for (const path of Object.values(swagger.paths)) {
24
+ if (path && typeof path === "object") {
25
+ for (const method of Object.values(path)) {
26
+ if (method &&
27
+ typeof method === "object" &&
28
+ Array.isArray(method.tags)) {
29
+ method.tags.forEach((tag) => tags.add(tag));
30
+ }
31
+ }
32
+ }
33
+ }
34
+ }
35
+ return Array.from(tags).sort();
36
+ }
37
+ export function registerGetServices(server) {
38
+ server.registerTool("getServices", {
39
+ title: "서비스 목록 조회",
40
+ description: "서비스 목록을 조회합니다.",
41
+ }, async () => {
42
+ const swagger = await loadSwagger();
43
+ const apiGroups = extractApiGroups(swagger);
44
+ return {
45
+ content: [{ type: "text", text: JSON.stringify(apiGroups, null, 2) }],
46
+ };
47
+ });
48
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * 환경 변수 파싱 및 검증 유틸리티
3
+ */
4
+ /**
5
+ * URL 유효성 검증
6
+ */
7
+ function isValidUrl(url) {
8
+ try {
9
+ const parsed = new URL(url);
10
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
11
+ }
12
+ catch {
13
+ return false;
14
+ }
15
+ }
16
+ /**
17
+ * OPENAPI_URL 환경 변수를 파싱하여 URL 배열로 반환
18
+ *
19
+ * 지원 형식:
20
+ * 1. JSON 배열: ["https://api1.com/openapi.json","https://api2.com/openapi.json"]
21
+ * 2. 단일 문자열: https://api.example.com/openapi.json (하위 호환성)
22
+ *
23
+ * @returns URL 배열
24
+ * @throws Error - 파싱 실패 또는 유효하지 않은 URL이 있는 경우
25
+ */
26
+ export function parseOpenApiUrls(envValue) {
27
+ if (!envValue) {
28
+ throw new Error("OPENAPI_URL 환경 변수가 설정되지 않았습니다. 서버를 시작할 수 없습니다.\n" +
29
+ "환경 변수를 설정한 후 다시 시도해주세요.\n" +
30
+ '예: export OPENAPI_URL=["https://api.example.com/openapi.json"]\n' +
31
+ "또는: export OPENAPI_URL=https://api.example.com/openapi.json");
32
+ }
33
+ let urls;
34
+ // JSON 배열 형식인지 확인
35
+ const trimmed = envValue.trim();
36
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
37
+ try {
38
+ const parsed = JSON.parse(trimmed);
39
+ // 배열인지 확인
40
+ if (!Array.isArray(parsed)) {
41
+ throw new Error("OPENAPI_URL이 JSON 배열 형식이지만 배열이 아닙니다.\n" +
42
+ '올바른 형식: ["https://api1.com/openapi.json","https://api2.com/openapi.json"]');
43
+ }
44
+ // 각 요소가 문자열인지 확인
45
+ if (!parsed.every((item) => typeof item === "string")) {
46
+ throw new Error("OPENAPI_URL 배열의 모든 요소는 문자열이어야 합니다.\n" +
47
+ '올바른 형식: ["https://api1.com/openapi.json","https://api2.com/openapi.json"]');
48
+ }
49
+ urls = parsed;
50
+ }
51
+ catch (error) {
52
+ if (error instanceof SyntaxError) {
53
+ throw new Error("OPENAPI_URL의 JSON 형식이 올바르지 않습니다.\n" +
54
+ '올바른 형식: ["https://api1.com/openapi.json","https://api2.com/openapi.json"]\n' +
55
+ `오류: ${error.message}`);
56
+ }
57
+ throw error;
58
+ }
59
+ }
60
+ else {
61
+ // 단일 문자열 형식 (하위 호환성)
62
+ urls = [trimmed];
63
+ }
64
+ // 빈 배열 검증
65
+ if (urls.length === 0) {
66
+ throw new Error("OPENAPI_URL 배열이 비어있습니다. 최소 1개 이상의 URL이 필요합니다.\n" +
67
+ '올바른 형식: ["https://api1.com/openapi.json","https://api2.com/openapi.json"]');
68
+ }
69
+ // 중복 제거
70
+ urls = [...new Set(urls)];
71
+ // 각 URL 유효성 검증
72
+ const invalidUrls = [];
73
+ urls.forEach((url, index) => {
74
+ if (!isValidUrl(url)) {
75
+ invalidUrls.push(`인덱스 ${index}: "${url}"`);
76
+ }
77
+ });
78
+ if (invalidUrls.length > 0) {
79
+ throw new Error("OPENAPI_URL에 유효하지 않은 URL이 있습니다:\n" +
80
+ invalidUrls.join("\n") +
81
+ "\n\n모든 URL은 http:// 또는 https://로 시작하는 유효한 URL이어야 합니다.");
82
+ }
83
+ return urls;
84
+ }