@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 +8 -0
- package/dist/swagger/apiTransformer.js +96 -0
- package/dist/swagger/schemaResolver.js +84 -0
- package/dist/swagger/swaggerLoader.js +3 -13
- package/dist/swagger/types.js +4 -0
- package/dist/tools/getApiDetail.tool.js +4 -45
- package/openapi.json +1138 -8
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
cachedAt = Date.now();
|
|
23
|
-
return cache;
|
|
13
|
+
return JSON.parse(fileContent);
|
|
24
14
|
}
|
|
25
15
|
catch (error) {
|
|
26
16
|
if (error instanceof Error) {
|
|
@@ -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
|
|
24
|
-
if (!
|
|
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
|
-
|
|
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
|
};
|