smbc-mcp-server 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 +121 -0
- package/build/config.js +6 -0
- package/build/error-sanitizer.js +21 -0
- package/build/http-client.js +90 -0
- package/build/index.js +43 -0
- package/build/openapi-loader.js +122 -0
- package/build/openapi-type.yml +11391 -0
- package/build/resource-handler.js +35 -0
- package/build/schema-converter.js +17 -0
- package/build/server.js +52 -0
- package/build/tool-generator.js +29 -0
- package/build/validator.js +41 -0
- package/package.json +30 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const OFFICIAL_DOCS_URL = "https://docs.smbc-gp.co.jp/mulpay/docs/introduction/overview";
|
|
2
|
+
const FALLBACK_MESSAGE = `詳細は公式ドキュメントを参照してください: ${OFFICIAL_DOCS_URL}`;
|
|
3
|
+
export function buildIndex(operations) {
|
|
4
|
+
const list = operations.map((op) => ({
|
|
5
|
+
operationId: op.operationId,
|
|
6
|
+
method: op.method.toUpperCase(),
|
|
7
|
+
path: op.path,
|
|
8
|
+
summary: op.summary,
|
|
9
|
+
requiresAuth: op.requiresAuth,
|
|
10
|
+
}));
|
|
11
|
+
if (list.length === 0) {
|
|
12
|
+
return JSON.stringify({ operations: [], message: FALLBACK_MESSAGE }, null, 2);
|
|
13
|
+
}
|
|
14
|
+
return JSON.stringify({ operations: list }, null, 2);
|
|
15
|
+
}
|
|
16
|
+
export function buildOperationDetail(operations, operationId) {
|
|
17
|
+
const op = operations.find((o) => o.operationId === operationId);
|
|
18
|
+
if (!op) {
|
|
19
|
+
return JSON.stringify({ error: `Operation not found: ${operationId}`, message: FALLBACK_MESSAGE }, null, 2);
|
|
20
|
+
}
|
|
21
|
+
return JSON.stringify({
|
|
22
|
+
operationId: op.operationId,
|
|
23
|
+
method: op.method.toUpperCase(),
|
|
24
|
+
path: op.path,
|
|
25
|
+
summary: op.summary,
|
|
26
|
+
description: op.description ?? null,
|
|
27
|
+
serverUrl: op.serverUrl ?? null,
|
|
28
|
+
defaultServerUrl: op.defaultServerUrl,
|
|
29
|
+
requestContentType: op.requestContentType,
|
|
30
|
+
requestSchema: op.requestSchema ?? null,
|
|
31
|
+
requiredFields: op.requiredFields ?? [],
|
|
32
|
+
hasIdempotencyKey: op.hasIdempotencyKey,
|
|
33
|
+
requiresAuth: op.requiresAuth,
|
|
34
|
+
}, null, 2);
|
|
35
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function toInputSchema(resolvedSchema, hasIdempotencyKey) {
|
|
2
|
+
const base = resolvedSchema
|
|
3
|
+
? JSON.parse(JSON.stringify(resolvedSchema))
|
|
4
|
+
: { type: "object", properties: {} };
|
|
5
|
+
if (!hasIdempotencyKey)
|
|
6
|
+
return base;
|
|
7
|
+
// properties が存在しない場合は追加
|
|
8
|
+
if (!base["properties"] || typeof base["properties"] !== "object") {
|
|
9
|
+
base["properties"] = {};
|
|
10
|
+
}
|
|
11
|
+
base["properties"]["idempotencyKey"] = {
|
|
12
|
+
type: "string",
|
|
13
|
+
maxLength: 36,
|
|
14
|
+
description: "冪等キー。リクエスト毎にユニークなUUID v4を推奨(最大36桁)。同一キーで安全にリトライ可能。",
|
|
15
|
+
};
|
|
16
|
+
return base;
|
|
17
|
+
}
|
package/build/server.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createServer } from "http";
|
|
2
|
+
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
3
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5
|
+
import { ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { join, dirname } from "path";
|
|
8
|
+
import { loadOperations } from "./openapi-loader.js";
|
|
9
|
+
import { buildIndex, buildOperationDetail } from "./resource-handler.js";
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const openapiPath = process.env.SMBC_OPENAPI_PATH ?? join(__dirname, "openapi-type.yml");
|
|
12
|
+
const port = process.env.PORT ? parseInt(process.env.PORT) : 3000;
|
|
13
|
+
// 起動時に一度だけ読み込んでキャッシュ
|
|
14
|
+
const { operations } = await loadOperations(openapiPath);
|
|
15
|
+
const app = createMcpExpressApp({ host: "0.0.0.0" });
|
|
16
|
+
app.post("/mcp", async (req, res) => {
|
|
17
|
+
const server = new Server({ name: "smbc-api-docs", version: "1.0.0" }, { capabilities: { resources: {} } });
|
|
18
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
19
|
+
return {
|
|
20
|
+
resources: [
|
|
21
|
+
{
|
|
22
|
+
uri: "smbc://index",
|
|
23
|
+
name: "SMBC API Index",
|
|
24
|
+
description: "全APIオペレーションのoperationId・メソッド・パス・summary一覧",
|
|
25
|
+
mimeType: "application/json",
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
31
|
+
const uri = request.params.uri;
|
|
32
|
+
if (uri === "smbc://index") {
|
|
33
|
+
return {
|
|
34
|
+
contents: [{ uri, mimeType: "application/json", text: buildIndex(operations) }],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const match = uri.match(/^smbc:\/\/operation\/(.+)$/);
|
|
38
|
+
if (match) {
|
|
39
|
+
const operationId = match[1];
|
|
40
|
+
return {
|
|
41
|
+
contents: [{ uri, mimeType: "application/json", text: buildOperationDetail(operations, operationId) }],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
throw new Error(`Unknown resource URI: ${uri}`);
|
|
45
|
+
});
|
|
46
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
47
|
+
await server.connect(transport);
|
|
48
|
+
await transport.handleRequest(req, res, req.body);
|
|
49
|
+
});
|
|
50
|
+
createServer(app).listen(port, () => {
|
|
51
|
+
process.stderr.write(`SMBC MCP Resource Server running on port ${port}\n`);
|
|
52
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { toInputSchema } from "./schema-converter.js";
|
|
2
|
+
import { buildZodSchema } from "./validator.js";
|
|
3
|
+
function buildDescription(op) {
|
|
4
|
+
if (!op.description)
|
|
5
|
+
return op.summary;
|
|
6
|
+
// description の最初の段落のみ(HTML タグ・Markdown除去)
|
|
7
|
+
const firstParagraph = op.description
|
|
8
|
+
.replace(/<[^>]*>/g, "") // HTMLタグ除去
|
|
9
|
+
.split(/\n\n/)[0] // 最初の段落のみ
|
|
10
|
+
.replace(/\s+/g, " ") // 連続スペース正規化
|
|
11
|
+
.trim();
|
|
12
|
+
if (!firstParagraph)
|
|
13
|
+
return op.summary;
|
|
14
|
+
return `${op.summary}\n${firstParagraph}`;
|
|
15
|
+
}
|
|
16
|
+
export function generateTools(operations) {
|
|
17
|
+
const tools = [];
|
|
18
|
+
const operationsMap = new Map();
|
|
19
|
+
for (const op of operations) {
|
|
20
|
+
tools.push({
|
|
21
|
+
name: op.operationId,
|
|
22
|
+
description: buildDescription(op),
|
|
23
|
+
inputSchema: toInputSchema(op.requestSchema, op.hasIdempotencyKey),
|
|
24
|
+
zodSchema: buildZodSchema(op.requestSchema, op.requiredFields, op.hasIdempotencyKey),
|
|
25
|
+
});
|
|
26
|
+
operationsMap.set(op.operationId, op);
|
|
27
|
+
}
|
|
28
|
+
return { tools, operations: operationsMap };
|
|
29
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
function toZodField(fieldSchema) {
|
|
3
|
+
if (!fieldSchema || typeof fieldSchema !== "object")
|
|
4
|
+
return z.unknown();
|
|
5
|
+
const s = fieldSchema;
|
|
6
|
+
const type = s["type"];
|
|
7
|
+
if (type === "string")
|
|
8
|
+
return z.string();
|
|
9
|
+
if (type === "integer" || type === "number")
|
|
10
|
+
return z.number();
|
|
11
|
+
if (type === "boolean")
|
|
12
|
+
return z.boolean();
|
|
13
|
+
return z.unknown();
|
|
14
|
+
}
|
|
15
|
+
export function buildZodSchema(schema, required, hasIdempotencyKey) {
|
|
16
|
+
const s = schema;
|
|
17
|
+
const properties = s?.["properties"];
|
|
18
|
+
if (!properties || typeof properties !== "object") {
|
|
19
|
+
return z.object({}).passthrough();
|
|
20
|
+
}
|
|
21
|
+
const shape = {};
|
|
22
|
+
for (const [key, fieldSchema] of Object.entries(properties)) {
|
|
23
|
+
const zodField = toZodField(fieldSchema);
|
|
24
|
+
shape[key] = required?.includes(key) ? zodField : zodField.optional();
|
|
25
|
+
}
|
|
26
|
+
if (hasIdempotencyKey) {
|
|
27
|
+
shape["idempotencyKey"] = z.string().max(36).optional();
|
|
28
|
+
}
|
|
29
|
+
return z.object(shape);
|
|
30
|
+
}
|
|
31
|
+
export function validateArgs(zodSchema, args) {
|
|
32
|
+
const result = zodSchema.safeParse(args);
|
|
33
|
+
if (result.success) {
|
|
34
|
+
return { success: true };
|
|
35
|
+
}
|
|
36
|
+
const errors = result.error.issues.map((issue) => {
|
|
37
|
+
const path = issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
38
|
+
return `${path}: ${issue.message}`;
|
|
39
|
+
});
|
|
40
|
+
return { success: false, errors };
|
|
41
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "smbc-mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "SMBC マルチペイメントサービス OpenAPI仕様をMCPリソースとして公開するサーバー",
|
|
5
|
+
"keywords": ["mcp", "smbc", "openapi", "payment", "modelcontextprotocol"],
|
|
6
|
+
"author": "",
|
|
7
|
+
"license": "ISC",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "index.js",
|
|
10
|
+
"bin": {
|
|
11
|
+
"smbc-mcp-server": "./build/index.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"build"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc && cp openapi-type.yml build/openapi-type.yml && chmod 755 build/index.js",
|
|
18
|
+
"test": "vitest run"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
22
|
+
"yaml": "^2.9.0",
|
|
23
|
+
"zod": "^3.25.76"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^25.9.1",
|
|
27
|
+
"typescript": "^6.0.3",
|
|
28
|
+
"vitest": "^4.1.7"
|
|
29
|
+
}
|
|
30
|
+
}
|