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 ADDED
@@ -0,0 +1,121 @@
1
+ # SMBC マルチペイメントサービス MCP サーバー
2
+
3
+ SMBC マルチペイメントサービス(OpenAPI タイプ)の API 仕様を MCP リソースとして公開するサーバーです。
4
+
5
+ Cursor・Claude Desktop などの MCP クライアントから API 仕様を参照できるようになります。クローンやビルドは不要で、`npx` で即利用できます。
6
+
7
+ 公式ドキュメント: https://docs.smbc-gp.co.jp/mulpay/docs/introduction/overview
8
+
9
+ ---
10
+
11
+ ## セットアップ
12
+
13
+ ### Cursor への登録
14
+
15
+ グローバル設定(`~/.cursor/mcp.json`)またはプロジェクト設定(`.cursor/mcp.json`)に以下を追加します。
16
+
17
+ ```json
18
+ {
19
+ "mcpServers": {
20
+ "smbc-api-docs": {
21
+ "command": "npx",
22
+ "args": ["smbc-mcp-server"],
23
+ "env": {
24
+ "SMBC_SHOP_ID": "your_shop_id",
25
+ "SMBC_SHOP_PASSWORD": "your_shop_password"
26
+ }
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ ### Claude Desktop への登録
33
+
34
+ `~/Library/Application Support/Claude/claude_desktop_config.json` に以下を追加します。
35
+
36
+ ```json
37
+ {
38
+ "mcpServers": {
39
+ "smbc-api-docs": {
40
+ "command": "npx",
41
+ "args": ["smbc-mcp-server"],
42
+ "env": {
43
+ "SMBC_SHOP_ID": "your_shop_id",
44
+ "SMBC_SHOP_PASSWORD": "your_shop_password"
45
+ }
46
+ }
47
+ }
48
+ }
49
+ ```
50
+
51
+ 設定後に MCP クライアントを再起動すると利用可能になります。
52
+
53
+ ---
54
+
55
+ ## 環境変数
56
+
57
+ | 変数名 | 必須 | 説明 | デフォルト |
58
+ |---|---|---|---|
59
+ | `SMBC_SHOP_ID` | 必須 | ショップID | - |
60
+ | `SMBC_SHOP_PASSWORD` | 必須 | ショップパスワード | - |
61
+ | `SMBC_OPENAPI_PATH` | 任意 | OpenAPI ファイルのパス | パッケージ同梱の `openapi-type.yml` |
62
+
63
+ ---
64
+
65
+ ## 利用できるリソース
66
+
67
+ ### `smbc://index`
68
+
69
+ 全 API オペレーションの一覧を返します。
70
+
71
+ ```json
72
+ {
73
+ "operations": [
74
+ { "operationId": "token", "method": "POST", "path": "/token", "summary": "アクセストークン発行", "requiresAuth": false },
75
+ { "operationId": "creditCharge", "method": "POST", "path": "/credit/charge", "summary": "クレジットカード決済", "requiresAuth": true }
76
+ ]
77
+ }
78
+ ```
79
+
80
+ ### `smbc://operation/{operationId}`
81
+
82
+ 特定の API オペレーションの詳細を返します。
83
+
84
+ ```
85
+ smbc://operation/token
86
+ smbc://operation/creditCharge
87
+ ```
88
+
89
+ レスポンスには `description`・`requestSchema`・`requiredFields`・`requiresAuth` などの完全な情報が含まれます。
90
+
91
+ 情報が見つからない場合は公式ドキュメント(https://docs.smbc-gp.co.jp/mulpay/docs/introduction/overview)への案内が返されます。
92
+
93
+ ---
94
+
95
+ ## API が更新された場合
96
+
97
+ 1. 新しいバージョンの本パッケージが公開されるまで待つ
98
+ 2. または `SMBC_OPENAPI_PATH` 環境変数で最新の YML ファイルを直接指定する
99
+
100
+ ```json
101
+ "env": {
102
+ "SMBC_SHOP_ID": "your_shop_id",
103
+ "SMBC_SHOP_PASSWORD": "your_shop_password",
104
+ "SMBC_OPENAPI_PATH": "/path/to/latest/openapi-type.yml"
105
+ }
106
+ ```
107
+
108
+ ---
109
+
110
+ ## 開発者向け
111
+
112
+ ```bash
113
+ # 依存インストール
114
+ npm install
115
+
116
+ # ビルド
117
+ npm run build
118
+
119
+ # テスト
120
+ npm test
121
+ ```
@@ -0,0 +1,6 @@
1
+ import { fileURLToPath } from "url";
2
+ import { join, dirname } from "path";
3
+ const __dirname = dirname(fileURLToPath(import.meta.url));
4
+ export function getOpenapiPath() {
5
+ return process.env.SMBC_OPENAPI_PATH ?? join(__dirname, "openapi-type.yml");
6
+ }
@@ -0,0 +1,21 @@
1
+ export function isApiError(result) {
2
+ return (result.startsWith("Error ") ||
3
+ result === "Network error occurred" ||
4
+ result === "Request timed out");
5
+ }
6
+ export function sanitizeError(e) {
7
+ if (e instanceof Error && e.name === "AbortError") {
8
+ return "Request timed out";
9
+ }
10
+ return "Network error occurred";
11
+ }
12
+ export function sanitizeHttpError(status, body) {
13
+ if (body !== null && typeof body === "object") {
14
+ const obj = body;
15
+ const title = typeof obj["title"] === "string" ? obj["title"] : undefined;
16
+ if (title) {
17
+ return `Error ${status}: ${title}`;
18
+ }
19
+ }
20
+ return `Error ${status}: request failed`;
21
+ }
@@ -0,0 +1,90 @@
1
+ import { sanitizeError, sanitizeHttpError } from "./error-sanitizer.js";
2
+ const MAX_ATTEMPTS = 3;
3
+ const BASE_DELAY_MS = 1000;
4
+ function sleep(ms) {
5
+ return new Promise((resolve) => setTimeout(resolve, ms));
6
+ }
7
+ function buildBasicAuth(shopId, shopPassword) {
8
+ const encoded = Buffer.from(`${shopId}:${shopPassword}`).toString("base64");
9
+ return `Basic ${encoded}`;
10
+ }
11
+ export async function callApi(options, config) {
12
+ const { method, serverUrl, path, body, contentType, idempotencyKey, requiresAuth } = options;
13
+ const url = `${serverUrl}${path}`;
14
+ const headers = {
15
+ "Content-Type": contentType,
16
+ };
17
+ // 認証ヘッダー
18
+ if (requiresAuth) {
19
+ if (config.accessToken) {
20
+ headers["Authorization"] = `Bearer ${config.accessToken}`;
21
+ }
22
+ else {
23
+ headers["Authorization"] = buildBasicAuth(config.shopId, config.shopPassword);
24
+ }
25
+ }
26
+ // 冪等キー
27
+ if (idempotencyKey) {
28
+ headers["Idempotency-Key"] = idempotencyKey;
29
+ }
30
+ // ボディ構築
31
+ let requestBody;
32
+ if (contentType === "application/x-www-form-urlencoded") {
33
+ const params = new URLSearchParams();
34
+ for (const [k, v] of Object.entries(body)) {
35
+ if (v != null)
36
+ params.append(k, String(v));
37
+ }
38
+ requestBody = params;
39
+ }
40
+ else {
41
+ requestBody = JSON.stringify(body);
42
+ }
43
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
44
+ // タイムアウトは各試行でリセット
45
+ const controller = new AbortController();
46
+ const timer = setTimeout(() => controller.abort(), 30_000);
47
+ try {
48
+ const response = await fetch(url, {
49
+ method: method.toUpperCase(),
50
+ headers,
51
+ body: Object.keys(body).length > 0 ? requestBody : undefined,
52
+ signal: controller.signal,
53
+ });
54
+ clearTimeout(timer);
55
+ const responseText = await response.text();
56
+ let responseData;
57
+ try {
58
+ responseData = JSON.parse(responseText);
59
+ }
60
+ catch {
61
+ responseData = responseText;
62
+ }
63
+ if (response.ok) {
64
+ return JSON.stringify(responseData, null, 2);
65
+ }
66
+ // 4xx: リトライしない
67
+ if (response.status < 500) {
68
+ return sanitizeHttpError(response.status, responseData);
69
+ }
70
+ // 5xx: 最終試行ならエラー返却、それ以外は backoff
71
+ if (attempt === MAX_ATTEMPTS - 1) {
72
+ return sanitizeHttpError(response.status, responseData);
73
+ }
74
+ await sleep(BASE_DELAY_MS * Math.pow(2, attempt));
75
+ }
76
+ catch (e) {
77
+ clearTimeout(timer);
78
+ // タイムアウト: リトライしない
79
+ if (e instanceof Error && e.name === "AbortError") {
80
+ return sanitizeError(e);
81
+ }
82
+ // ネットワークエラー: 最終試行ならエラー返却、それ以外は backoff
83
+ if (attempt === MAX_ATTEMPTS - 1) {
84
+ return sanitizeError(e);
85
+ }
86
+ await sleep(BASE_DELAY_MS * Math.pow(2, attempt));
87
+ }
88
+ }
89
+ return sanitizeError(new Error("Unexpected retry exhaustion"));
90
+ }
package/build/index.js ADDED
@@ -0,0 +1,43 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
4
+ import { getOpenapiPath } from "./config.js";
5
+ import { loadOperations } from "./openapi-loader.js";
6
+ import { buildIndex, buildOperationDetail } from "./resource-handler.js";
7
+ // 1. OpenAPI仕様書からエンドポイントを抽出
8
+ const { operations } = await loadOperations(getOpenapiPath());
9
+ // 2. MCPサーバー初期化
10
+ const server = new Server({ name: "smbc-multipayment", version: "1.0.0" }, { capabilities: { resources: {} } });
11
+ // 3. リソース一覧ハンドラー
12
+ server.setRequestHandler(ListResourcesRequestSchema, async () => {
13
+ return {
14
+ resources: [
15
+ {
16
+ uri: "smbc://index",
17
+ name: "SMBC API Index",
18
+ description: "全APIオペレーションのoperationId・メソッド・パス・summary一覧",
19
+ mimeType: "application/json",
20
+ },
21
+ ],
22
+ };
23
+ });
24
+ // 4. リソース読み取りハンドラー
25
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
26
+ const uri = request.params.uri;
27
+ if (uri === "smbc://index") {
28
+ return {
29
+ contents: [{ uri, mimeType: "application/json", text: buildIndex(operations) }],
30
+ };
31
+ }
32
+ const match = uri.match(/^smbc:\/\/operation\/(.+)$/);
33
+ if (match) {
34
+ const operationId = match[1];
35
+ return {
36
+ contents: [{ uri, mimeType: "application/json", text: buildOperationDetail(operations, operationId) }],
37
+ };
38
+ }
39
+ throw new Error(`Unknown resource URI: ${uri}`);
40
+ });
41
+ // 5. 起動
42
+ const transport = new StdioServerTransport();
43
+ await server.connect(transport);
@@ -0,0 +1,122 @@
1
+ import { readFile } from "fs/promises";
2
+ import { parse } from "yaml";
3
+ function resolveRefs(schema, schemas, resolving = new Set()) {
4
+ if (!schema || typeof schema !== "object")
5
+ return schema;
6
+ const obj = schema;
7
+ // $ref の解決
8
+ if (typeof obj["$ref"] === "string") {
9
+ const ref = obj["$ref"];
10
+ // '#/components/schemas/SchemaName' 形式のみ対応
11
+ const match = ref.match(/^#\/components\/schemas\/(.+)$/);
12
+ if (!match)
13
+ return schema;
14
+ const name = match[1];
15
+ if (resolving.has(name))
16
+ return {}; // 循環参照ガード
17
+ const target = schemas[name];
18
+ if (!target)
19
+ return schema;
20
+ const next = new Set(resolving);
21
+ next.add(name);
22
+ return resolveRefs(target, schemas, next);
23
+ }
24
+ const result = { ...obj };
25
+ // properties
26
+ if (result["properties"] && typeof result["properties"] === "object") {
27
+ const props = result["properties"];
28
+ result["properties"] = Object.fromEntries(Object.entries(props).map(([k, v]) => [k, resolveRefs(v, schemas, resolving)]));
29
+ }
30
+ // items
31
+ if (result["items"]) {
32
+ result["items"] = resolveRefs(result["items"], schemas, resolving);
33
+ }
34
+ // oneOf / anyOf / allOf
35
+ for (const key of ["oneOf", "anyOf", "allOf"]) {
36
+ if (Array.isArray(result[key])) {
37
+ result[key] = result[key].map((s) => resolveRefs(s, schemas, resolving));
38
+ }
39
+ }
40
+ return result;
41
+ }
42
+ export async function loadOperations(filePath) {
43
+ let content;
44
+ try {
45
+ content = await readFile(filePath, "utf-8");
46
+ }
47
+ catch {
48
+ process.stderr.write(`Error: Cannot read OpenAPI file: ${filePath}\n`);
49
+ process.exit(1);
50
+ return { operations: [], defaultServerUrl: "" }; // unreachable, for type narrowing
51
+ }
52
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
53
+ let spec;
54
+ try {
55
+ spec = parse(content);
56
+ }
57
+ catch (e) {
58
+ process.stderr.write(`Error: Failed to parse YAML: ${e}\n`);
59
+ process.exit(1);
60
+ }
61
+ const schemas = spec?.components?.schemas ?? {};
62
+ const globalServers = spec?.servers ?? [];
63
+ const defaultServerUrl = globalServers[0]?.url ?? "";
64
+ const operations = [];
65
+ const paths = spec?.paths ?? {};
66
+ for (const [path, pathItem] of Object.entries(paths)) {
67
+ const pathObj = pathItem;
68
+ for (const method of ["get", "post", "put", "patch", "delete"]) {
69
+ if (!pathObj[method])
70
+ continue;
71
+ const op = pathObj[method];
72
+ const operationId = op["operationId"];
73
+ if (!operationId)
74
+ continue;
75
+ const summary = op["summary"] ?? "";
76
+ const description = op["description"];
77
+ // エンドポイント固有サーバー
78
+ const opServers = op["servers"];
79
+ const serverUrl = opServers?.[0]?.url;
80
+ // Idempotency-Key ヘッダー確認
81
+ const parameters = op["parameters"];
82
+ const hasIdempotencyKey = parameters?.some((p) => p["$ref"] === "#/components/parameters/headerIdempotencyKey") ?? false;
83
+ // 認証要否: security: [] は認証不要
84
+ const security = op["security"];
85
+ const requiresAuth = !(Array.isArray(security) && security.length === 0);
86
+ // リクエストボディのコンテンツタイプとスキーマ
87
+ const requestBody = op["requestBody"];
88
+ const content = requestBody?.["content"];
89
+ const contentType = content
90
+ ? (Object.keys(content).find((k) => k === "application/json") ??
91
+ Object.keys(content)[0] ??
92
+ "application/json")
93
+ : "application/json";
94
+ const rawSchema = content
95
+ ? content[contentType]?.["schema"]
96
+ : undefined;
97
+ const requestSchema = rawSchema
98
+ ? resolveRefs(rawSchema, schemas)
99
+ : undefined;
100
+ const resolvedSchema = requestSchema;
101
+ const requiredFields = Array.isArray(resolvedSchema?.["required"])
102
+ ? resolvedSchema["required"]
103
+ : undefined;
104
+ operations.push({
105
+ operationId,
106
+ summary,
107
+ description,
108
+ method,
109
+ path,
110
+ serverUrl,
111
+ defaultServerUrl,
112
+ requestContentType: contentType,
113
+ requestSchema,
114
+ requiredFields,
115
+ hasIdempotencyKey,
116
+ requiresAuth,
117
+ });
118
+ }
119
+ }
120
+ process.stderr.write(`Loaded ${operations.length} tools from ${filePath}\n`);
121
+ return { operations, defaultServerUrl };
122
+ }