[MCP Serverã«ã€ããŠèª¿ã¹ãŠã¿ãåå¿é²ïŒMCPãµãŒããŒã®æå°éã®å®è£
äŸïŒ - at backyard](https://shinshin86.hateblo.jp/entry/2025/03/09/093920#google_vignette)
[[ðïžMCP Servers|ðïžMCPãµãŒããŒ]]ã®ç°¡æå®è£
ãèŒã£ãŠããã
[[ðïžMCP Made Easy Cline]]ã¡ãããšèªãã§ããããšãªã£ãã
âââââ
çã䟿å©ã ãšèšã£ãŠãã[MCP](https://d.hatena.ne.jp/keyword/MCP)ã«ã€ããŠä»æŽãªããå
¥éããã®ã§åå¿é²ãšããŠæ®ããŠããïŒèªåçšã¡ã¢ïŒ
## [MCP](https://d.hatena.ne.jp/keyword/MCP)ãšã¯ïŒãªã話é¡ã«ãªã£ãŠããïŒ
[MCP](https://d.hatena.ne.jp/keyword/MCP)ïŒModel Context ProtocolïŒãšã¯ãLLMïŒå€§èŠæš¡[èšèªã¢ãã«](https://d.hatena.ne.jp/keyword/%B8%C0%B8%EC%A5%E2%A5%C7%A5%EB)ïŒãšå€éšããŒã«ã»ããŒã¿ãœãŒã¹ãã€ãªãããã®çµ±äžçãª[ãããã³ã«](https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%C8%A5%B3%A5%EB)ãæäŸããä»çµã¿ã
AIãšãŒãžã§ã³ããšããããå€éšã·ã¹ãã ãã€ãªããUSB-CããŒããã®ãããªãã®ãã€ã¡ãŒãžãããšè¯ãã
éåžžãAIã®å€éšé£æºïŒãã¡ã€ã«ã[ãªããžããª](https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA)ã[API](https://d.hatena.ne.jp/keyword/API)ã¢ã¯ã»ã¹ãªã©ïŒãè¡ãã«ã¯ã±ãŒã¹ããšã«å°çš[ã¹ã¯ãªãã](https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8)ãå¿
èŠã«ãªããã[MCP](https://d.hatena.ne.jp/keyword/MCP)ã«ãã£ãŠçµ±äžã€ã³ã¿ãŒãã§ãŒã¹ãä»ããæ¡åŒµãå¯èœã«ãªãã
ããã«ããã**ãšãã£ã¿ãAIã«ãå®éã«å€éšã®äœããå®è¡ãããã**ããšãæ Œæ®µã«å®¹æã«ãªããããšãã°ãã¡ã€ã«ã®èªã¿æžããDBã¯ãšãªã[API](https://d.hatena.ne.jp/keyword/API)åŒã³åºãããã¹ãã®å®è¡ããããã€ãCI/CDãšã®é£æºãªã©ãã[MCP](https://d.hatena.ne.jp/keyword/MCP)察å¿ã®[ãã©ã°ã€ã³](https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3)ïŒ[MCP](https://d.hatena.ne.jp/keyword/MCP)ãµãŒãïŒãšããŠäžå
åãããã
## [MCP](https://d.hatena.ne.jp/keyword/MCP)ã®åºæ¬
[MCP](https://d.hatena.ne.jp/keyword/MCP)ãšäžèšã§èšã£ãŠãã
- [MCP](https://d.hatena.ne.jp/keyword/MCP) Clinet
- [MCP](https://d.hatena.ne.jp/keyword/MCP) Server
ãšããã®ããã
[MCP](https://d.hatena.ne.jp/keyword/MCP) Clientã¯CursorãClaude DesktopãClineã®ãããªãã®ãæãã
ãŸããèªåã§[MCP](https://d.hatena.ne.jp/keyword/MCP) Clientãæžãããšãã§ãã
æ§æçã«ã¯ãã£ãã以äžã®ããã«ãªã£ãŠãããæ§æãšããŠã¯ãšãŠãã·ã³ãã«ã§åãããããã
```
MCP Client <---> MCP Server
```
äžéäžè¬çã«èšãããŠãã[MCP](https://d.hatena.ne.jp/keyword/MCP)ãšããã®ã¯å€§æµ[MCP](https://d.hatena.ne.jp/keyword/MCP) Serverã®ããšãæããŠãããããã§Cursorãªã©ã®[MCP](https://d.hatena.ne.jp/keyword/MCP) ClientïŒAIãšãŒãžã§ã³ãïŒããæž¡ã£ãŠããæç€ºãããšã«[MCP](https://d.hatena.ne.jp/keyword/MCP) ServeråŽã§ããããããšãå®è¡ããããšã«ãªãã
ïŒãã® "ããããããš" ã«æ§ã
ãªå¯èœæ§ãè©°ããããŠãããä»è©±é¡ã«ãªã£ãŠãããšãã圢ïŒ
ã¡ãªã¿ã«[MCP](https://d.hatena.ne.jp/keyword/MCP)é¢é£ã«ã€ããŠã¯ä»¥äžãèŠãŠããã°æäœéã®çè§£ã¯å¯èœããã
[modelcontextprotocol.io](https://modelcontextprotocol.io/introduction)
ãŸããCursorã§äœ¿ãå Žåã¯CursoråŽãåºããŠããããã¥ã¡ã³ããåç
§
[docs.cursor.com](https://docs.cursor.com/context/model-context-protocol)
Clineã§äœ¿ãå Žåã¯ClineãåºããŠããããã¥ã¡ã³ããåç
§
[docs.cline.bot](https://docs.cline.bot/mcp-servers/mcp-quickstart)
## [MCP](https://d.hatena.ne.jp/keyword/MCP)ãµãŒããŒã®æå°éã®å®è£
äŸ
[MCP](https://d.hatena.ne.jp/keyword/MCP) Serverã®åºæ¬çãªæåãçè§£ããã«ã¯ããŸãã¯ãã©ããªã³ãŒããå¿
èŠãªã®ããããã£ããèŠãŠã¿ãã®ãæ©ãã以äžã®ãµã³ãã«ã§ã¯ãå€éšã©ã€ãã©ãªã䜿ããã**æšæºå
¥åºåïŒSTDIN/STDOUTïŒ**ãä»ããŠ[MCP](https://d.hatena.ne.jp/keyword/MCP)ã¯ã©ã€ã¢ã³ãïŒCursorãªã©ïŒãšéä¿¡ããä»çµã¿ããçŽ100è¡çšåºŠã§å®è£
ããŠããã
ã[JSON](https://d.hatena.ne.jp/keyword/JSON)\-RPC 2.0ãæ±ããã[MCP](https://d.hatena.ne.jp/keyword/MCP)ãã³ãã·ã§ã€ã¯ãåŠçããããããŒã«åŒã³åºãã«å¿çããããšãã3ç¹ãæŒãããããã°ãã²ãšãŸãåã[MCP](https://d.hatena.ne.jp/keyword/MCP)ãµãŒããŒã«ãªãã®ãåãããšæãã
â»TypeScriptã§æžããŠããã®ã§ãå®éã«åããéã¯buildã㊠`js` ã«ããã`node /foo/bar/simple-mcp-server.js` ãšãã圢ã§CursoråŽã§ã¯ããã»ã¹ãèµ·åããŠåããæ³å®
```typescript
/**
* ã·ã³ãã«ãªMCPãµãŒã㌠(å€éšã©ã€ãã©ãªæªäœ¿çšç)
*
* ãã®ãµãŒããŒã¯Cursorãªã©ã®MCPã¯ã©ã€ã¢ã³ããšæšæºå
¥åºå(STDIN/STDOUT)ãéããŠ
* JSON-RPC圢åŒã§éä¿¡ããMCPã®æäœéã®æ©èœãæäŸããŸãã
*
* ä»åã¯ãechoããŒã«ãã1ã€ã ãå®è£
ãããŠãŒã¶ãŒãéã£ãæååããã®ãŸãŸè¿ããµã³ãã«ã§ãã
*/
import * as readline from 'readline';
/** MCPããŒã«ã®åºåã³ã³ãã³ãã®åå®çŸ© */
interface ToolContent {
type: string;
text?: string;
}
/** MCPããŒã«ã®åºæ¬æ§é */
interface MCPTool {
name: string; // ããŒã«å
description: string; // ããŒã«ã®ç°¡åãªèª¬æ
inputSchema: object; // å
¥åãã©ã¡ãŒã¿ã®JSON Schema
execute: (args: any) => ToolContent | ToolContent[];
}
/** äŸ: EchoããŒã« - å
¥åæååããã®ãŸãŸè¿ã */
const echoTool: MCPTool = {
name: "echo",
description: "æå®ãããã¡ãã»ãŒãžããã®ãŸãŸè¿ãããŒã«",
inputSchema: {
type: "object",
properties: {
message: { type: "string", description: "ãšã³ãŒãããæåå" }
},
required: ["message"]
},
execute: (args) => {
const msg = args.message;
return { type: "text", text: \`Echo: ${msg}\` };
}
};
/** æäŸããããŒã«ã®äžèЧ */
const tools: MCPTool[] = [ echoTool ];
/** JSON-RPCã¬ã¹ãã³ã¹ãæšæºåºåã«éããŠãŒãã£ãªãã£é¢æ° */
function sendResponse(obj: any) {
process.stdout.write(JSON.stringify(obj) + "\n");
}
/** JSON-RPCãšã©ãŒåœ¢åŒã§è¿ããŠãŒãã£ãªãã£é¢æ° */
function sendError(id: any, code: number, message: string) {
const errorResponse = {
jsonrpc: "2.0",
id: id,
error: { code, message }
};
sendResponse(errorResponse);
}
/** æšæºå
¥å(STDIN)ãè¡åäœã§èªãããã®èšå® */
const rl = readline.createInterface({ input: process.stdin });
/**
* æšæºå
¥åã§åãåã£ãJSON-RPCãªã¯ãšã¹ããããŒã¹ãã
* MCPãããã³ã«ã«æ²¿ã£ãåŠçãè¡ãã
*/
rl.on('line', (data: string) => {
if (!data.trim()) return; // 空è¡ã¯ç¡èŠ
let request;
try {
request = JSON.parse(data);
} catch (err) {
// JSONã®ããŒã¹ã«å€±æããå Žåã¯ã-32700(Parse error)ãè¿ã
sendError(null, -32700, "Parse error");
return;
}
// JSON-RPC 2.0ã®ãã©ãŒããããæºãããŠããã確èª
if (request.jsonrpc !== "2.0" || !request.method) {
sendError(request.id ?? null, -32600, "Invalid Request");
return;
}
const method = request.method;
const id = request.id;
const isNotification = (id === null || id === undefined);
// 1. åæåãã³ãã·ã§ã€ã¯ (initialize â initialized)
if (method === "initialize") {
// ã¯ã©ã€ã¢ã³ããããããã³ã«ããŒãžã§ã³ãåãåãããµãŒããŒã®æ
å ±ãªã©ãè¿ã
const clientProtocol = request.params?.protocolVersion;
const protocolVersion = (typeof clientProtocol === "string")
? clientProtocol
: "2025-03-08"; // ããã©ã«ãããããã¯ææ°ã®æ¥æãªã©ãé©åœã«èšå®
const initResponse = {
jsonrpc: "2.0",
id: id,
result: {
protocolVersion: protocolVersion,
serverInfo: {
name: "simple-mcp-server",
version: "0.1.0"
},
// ãã®ãµãŒããŒãæäŸããæ©èœã®æŠèŠ (toolsããµããŒã)
capabilities: {
tools: {}
}
}
};
sendResponse(initResponse);
return;
}
if (method === "initialized" || method === "notifications/initialized") {
// ã¯ã©ã€ã¢ã³ãåŽããµãŒããŒã®è¿çãåãåããåæåå®äºããããšãéç¥ãã
// ããã§ã¯ç¹ã«ã¬ã¹ãã³ã¹ã¯è¿ããªã
return;
}
// 2. (ä»»æ) ãã£ã³ã»ã«éç¥ãªã©
if (method === "cancelled") {
// ããŒã«åŒã³åºãéäžã§ã¯ã©ã€ã¢ã³ãããã£ã³ã»ã«ãçºè¡ããå Žåãªã©
// æ¬ãµã³ãã«ã§ã¯äœãããªã
return;
}
// 3. ããŒã«äžèЧã®ååŸ
if (method === "tools/list") {
// çŸåšãµãŒããŒã«ç»é²ãããŠããããŒã«äžèЧãè¿ã
const toolList = tools.map(t => ({
name: t.name,
description: t.description,
inputSchema: t.inputSchema
}));
const listResponse = {
jsonrpc: "2.0",
id: id,
result: {
tools: toolList
}
};
sendResponse(listResponse);
return;
}
// ãªãœãŒã¹ãããã³ãããªã©ã¯ç©ºãªã¹ãã§è¿ã
if (method === "resources/list") {
sendResponse({ jsonrpc: "2.0", id: id, result: { resources: [] } });
return;
}
if (method === "prompts/list") {
sendResponse({ jsonrpc: "2.0", id: id, result: { prompts: [] } });
return;
}
// 4. ããŒã«åŒã³åºãèŠæ± (tools/call)
if (method === "tools/call") {
const params = request.params;
if (!params || typeof params !== "object") {
sendError(id, -32602, "Invalid parameters");
return;
}
const toolName = params.name;
const args = params.arguments;
if (typeof toolName !== "string" || args === undefined) {
sendError(id, -32602, "Invalid parameters: missing tool name or arguments");
return;
}
// ããŒã«åã«å¯Ÿå¿ããããŒã«ãæ¢ã
const tool = tools.find(t => t.name === toolName);
if (!tool) {
sendError(id, -32601, \`Method not found: tool '${toolName}' is not available\`);
return;
}
// ããŒã«ã®inputSchemaã§å¿
é ãã£ãŒã«ããããã°ãã§ãã¯
const schema: any = tool.inputSchema;
if (schema.required && Array.isArray(schema.required)) {
for (const field of schema.required) {
if (!(field in args)) {
sendError(id, -32602, \`Missing required parameter: '${field}'\`);
return;
}
}
}
// ããŒã«ã®executeãåŒã³åºã
try {
const resultContent = tool.execute(args);
// MCPãããã³ã«äžãcontentã¯é
åã§è¿ãå¿
èŠããã
const contentArray: ToolContent[] = Array.isArray(resultContent) ? resultContent : [ resultContent ];
// çµæãJSON-RPC圢åŒã§è¿åŽ
const callResponse = {
jsonrpc: "2.0",
id: id,
result: {
content: contentArray
}
};
sendResponse(callResponse);
} catch (error) {
// ããŒã«å®è¡äžã«äºæãã¬ãšã©ãŒãçºçããå Žå
sendError(id, -32603, "Internal error during tool execution");
}
return;
}
// 5. äžèšä»¥å€ã®ã¡ãœãããåŒã°ããå Žå
if (!isNotification) {
sendError(id, -32601, \`Method not found: ${method}\`);
}
});
/** MCPã¯ã©ã€ã¢ã³ããåæãããããã»ã¹ãçµäº */
rl.on('close', () => {
process.exit(0);
});
```
å®éã«Cursorã«[MCP](https://d.hatena.ne.jp/keyword/MCP) ServerãšããŠç»é²ããŠå®è¡ãããã®ã以äžã
ïŒTestServerãšããåç§°ã§ç»é²ããŠããïŒ

æç€ºã®æç« ã«èª€åãããã®ã¯åŸ¡æå¬...
### ã³ãŒã解説
以äžããã**æãã·ã³ãã«ãª[MCP](https://d.hatena.ne.jp/keyword/MCP)ãµãŒããŒå®è£
äŸ**ããšãªãã
1. **æšæºå
¥åºåã§[JSON](https://d.hatena.ne.jp/keyword/JSON)\-RPCãèªã¿æžããã**
- åé ã§ `readline` ã䜿ã£ãŠ `process.stdin` ãè¡åäœã§èªã¿èŸŒã¿ã`process.stdout.write` ã«ãã£ãŠã¬ã¹ãã³ã¹ãè¿ããŠããã
- [MCP](https://d.hatena.ne.jp/keyword/MCP)ã¯ã©ã€ã¢ã³ãïŒäŸãã°CursorïŒããã®ããã»ã¹ãèµ·åããæšæºå
¥åºåãéããŠã¡ãã»ãŒãžãéåä¿¡ããã€ã¡ãŒãžã
2. **[JSON](https://d.hatena.ne.jp/keyword/JSON)\-RPCãããŒã¹ãšãã[MCP](https://d.hatena.ne.jp/keyword/MCP)ã¡ãœããã«åå²**
- `initialize` â [ãããã³ã«](https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%C8%A5%B3%A5%EB)ããŒãžã§ã³ããµãŒããŒæ
å ±ã®åãæž¡ã
- `initialized` â ã¯ã©ã€ã¢ã³ãããã®åæåå®äºéç¥
- `tools/list` â ã©ããªããŒã«ããµãŒããŒã«ãããã®äžèЧ
- `tools/call` â å®éã«ããŒã«ãåŒã³åºããŠäœãåŠçãè¡ã
- ãªã©ã[MCP](https://d.hatena.ne.jp/keyword/MCP)ã§ã¯äžå®ã®ã¡ãœãããå®çŸ©ãããŠãããããããæ®éã®`if`åå²ã§åŠçããŠããã
3. **echoããŒã«ã®ã¿ãå®è£
**
- `tools` ãšããé
åã«ã`echo` ãšããååã®ããŒã«ã1ã€ã ã远å ããŠããã
- `tools/list` ãåŒã°ãããšããã®ããŒã«æ
å ±ïŒååã説æãinputSchemaïŒãè¿ãããã
- `tools/call` ã« `{ name: "echo", arguments: { message: "Hello" } }` ã®ãããªåŒæ°ãæž¡ããããšã`execute` 颿°ãåŒã°ã㊠`"Echo: Hello"` ãšããããã¹ããè¿ãã
4. **ãšã©ãŒåŠç**
- [JSON](https://d.hatena.ne.jp/keyword/JSON)ãããŒã¹ã§ããªãå Žåã¯`-32700(Parse error)`ã[MCP](https://d.hatena.ne.jp/keyword/MCP)ã¡ãœããã®åœ¢åŒãåããªãå Žåã¯`-32600(Invalid Request)`ããªã©ã[JSON](https://d.hatena.ne.jp/keyword/JSON)\-RPC 2.0ã®æšæºãšã©ãŒã³ãŒãã䜿ã£ãŠããã
- [MCP](https://d.hatena.ne.jp/keyword/MCP)ã§ã¯ãç¥ããªãã¡ãœããåã®å Žåã¯`-32601(Method not found)`ãè¿ãããšããã®ãåºæ¬ã«ãŒã«ã
5. **æäœéã®æ©èœ**
- ãªãœãŒã¹äžèЧ(`resources/list`)ãããã³ããäžèЧ(`prompts/list`)ã¯ç©ºãè¿ãã ããªã®ã§ãç¹ã«æ©èœãšããŠã¯å®è£
ããŠããªãã
- ããå°ãæ¡åŒµããããšãã¯åæ§ã«`if (method===\"resources/list\") { ... }` ã®äžã§è²ã
æžãã°OKã
---
ãã®ããã«ã**[MCP](https://d.hatena.ne.jp/keyword/MCP)ãµãŒããŒã®æå°éã®å®è£
**ã¯ã[JSON](https://d.hatena.ne.jp/keyword/JSON)\-RPCã¡ãœããã®åãåããšåå²åŠçããèªåã§æžãã ãã§ãå¯èœã ããããã«å€éšã©ã€ãã©ãªãå€éš[API](https://d.hatena.ne.jp/keyword/API)åŒã³åºããå ããŠããã°ã**ããããâ[MCP](https://d.hatena.ne.jp/keyword/MCP)[ãã©ã°ã€ã³](https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3)â**ãäœããããã«ãªãããã ã
ä»åã¯èªèº«ã®çè§£ã®ããã«ãæäœéã®å®è£
ãåãããŠã¿ãããåºæ¬çã«[MCP](https://d.hatena.ne.jp/keyword/MCP) Serverãèªåã§å®è£
ããã±ãŒã¹ã¯ãªãã [å
¬åŒã®Nodeåãã©ã€ãã©ãª](https://modelcontextprotocol.io/quickstart/server)ïŒ`@modelcontextprotocol/sdk`ïŒãªã©ã䜿ã£ãŠäœæããŠããã±ãŒã¹ãå€ãããšæãããã
ãŸãä»åã®æå°éã®å®è£
ã§ã¯æšæºå
¥åºåã®ã¿ã®å¯Ÿå¿ã ããå
¬åŒã©ã€ãã©ãªã§ã¯SSEãªã©ã«ã察å¿ããŠãããã[OAuthèªèšŒ](https://d.hatena.ne.jp/keyword/OAuth%C7%A7%BE%DA)ãZodã«ãã[ã¹ããŒã](https://d.hatena.ne.jp/keyword/%A5%B9%A5%AD%A1%BC%A5%DE)æ€èšŒãªã©ãå
¥ã£ãŠããã
ãã éã«ã**[JSON](https://d.hatena.ne.jp/keyword/JSON)\-RPC 2.0ã®ä»æ§ã«åºã¥ããŠããã°ã©ã€ãã©ãªããªããŠãå®è£
ã§ããããã奜ããªèšèªã§[MCP](https://d.hatena.ne.jp/keyword/MCP)ãµãŒããŒãæžãããšãå¯èœã§ãããšãèšããã**
ãããã£ããããçšåºŠéçºè
ã«èªç±ãããç°å¢ãšããã®ã¯ãä»åŸ[MCP](https://d.hatena.ne.jp/keyword/MCP)ãçãäžãã£ãŠãããããªèŠå ã®äžã€ã«ãªãããªãšæãã
ïŒã©ã®èšèªã§ãå®è£
ã§ããã¡ãªãããšããŠä»ã«ããGoãRustãªã©ã§å®è£
ããŠã·ã³ã°ã«ãã€ããªã§å®è¡ã§ãã圢ã§é
åžããã°ããŠãŒã¶ãŒåŽã§Node.jsã[Python](https://d.hatena.ne.jp/keyword/Python)ç°å¢ãçšæããŠãããå¿
èŠããªãïŒ
## [MCP](https://d.hatena.ne.jp/keyword/MCP) Marketplaceã®ååšïŒClineïŒ
Clineã«ã¯[MCP](https://d.hatena.ne.jp/keyword/MCP) Marketplaceãšããååšããããããã [VSCode](https://d.hatena.ne.jp/keyword/VSCode)ã®Extension marketplaceã®ãããªãã®ã
[cline.bot](https://cline.bot/mcp-marketplace)
ãªãªãŒã¹ããã«ã¯[GitHub](https://d.hatena.ne.jp/keyword/GitHub)çµç±ã§æåºããŠå¯©æ»ãåããå¿
èŠãããã
## ãã®ä»ã[MCP](https://d.hatena.ne.jp/keyword/MCP) ServerããŸãšããããŠãããµã€ã
ä»ã«ããããããµã€ããããã¿ãã
[mcpserver.cc](https://mcpserver.cc/)
[MCP](https://d.hatena.ne.jp/keyword/MCP) Serverã¯ããããšæãã°æ§ã
ãªããšãã§ããŠããŸãããå€éšã®[MCP](https://d.hatena.ne.jp/keyword/MCP)ã䜿ãå Žåã®å®å
šæ§ã®ç·åŒãã©ãããžãã«ããã®ããç§ã¯ãŸã 枩床æãæŽããŠããªãã
## [MCP](https://d.hatena.ne.jp/keyword/MCP)ã話é¡ã«ãªã£ãŠããçç±ãåãã£ãæ°ããã
[MCP](https://d.hatena.ne.jp/keyword/MCP) ClinetãæçžŠåžã ãšãããšãå®éã®å®åãè¡ãªãã®ã[MCP](https://d.hatena.ne.jp/keyword/MCP) Serverãšãªãã ãããããããã«ãã®ä»çµã¿ã䜿ãã°AIã«æç€ºãåºããŠãããããšãäžæ°ã«å¢ããããå¯èœæ§ã溢ããŠãããã
[MCP](https://d.hatena.ne.jp/keyword/MCP)ã話é¡ã«ãªã£ãŠããã®ãå°ãã ãåãã£ãæ°ãããã
Prev. entry (2025-03-04)[

](https://shinshin86.hateblo.jp/entry/2025/03/04/181314)
[Archive](https://shinshin86.hateblo.jp/)
[](https://hatena.blog/?utm_source=https%3A%2F%2Fshinshin86.hateblo.jp%2Fentry%2F2025%2F03%2F09%2F093920&utm_medium=referral&utm_campaign=articleBar)