@koseha/api-mcp 0.0.8 → 0.0.10

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/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,26 +1,16 @@
1
1
  /**
2
2
  * openapi.json HTTP 요청으로 읽기 + TTL 캐시
3
3
  */
4
- const DEFAULT_OPENAPI_URL = "https://petstore3.swagger.io/api/v3/openapi.json";
5
- let cache = null;
6
- let cachedAt = 0;
7
- const TTL = 5 * 60 * 1000; // 5분
4
+ const OPENAPI_URL = process.env.OPENAPI_URL;
8
5
  export async function loadSwagger() {
9
- if (cache && Date.now() - cachedAt < TTL) {
10
- return cache;
11
- }
12
- const openapiUrl = process.env.OPENAPI_URL ||
13
- process.env.OPENAPI_JSON_URL ||
14
- DEFAULT_OPENAPI_URL;
6
+ const openapiUrl = OPENAPI_URL;
15
7
  try {
16
8
  const response = await fetch(openapiUrl);
17
9
  if (!response.ok) {
18
10
  throw new Error(`HTTP ${response.status}: ${response.statusText} - ${openapiUrl}`);
19
11
  }
20
12
  const fileContent = await response.text();
21
- cache = JSON.parse(fileContent);
22
- cachedAt = Date.now();
23
- return cache;
13
+ return JSON.parse(fileContent);
24
14
  }
25
15
  catch (error) {
26
16
  if (error instanceof Error) {
@@ -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
  };