168
150

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[MCP再入門]「MCPはAIアプリにとってのUSB-C」がしっくりこなかったあなたに

Last updated at Posted at 2025-04-13

はじめに

 MCPとは?公式ページの冒頭にはUSB-Cみたいな共通仕様、という抽象的な説明があります。この例から端を発したMCPの説明を日本語で書かれているのもいくつか見かけますし、記事としてとても人気があるようです。

 ですが、公式ページを読んでも、日本語で書かれたいろんな紹介ページを読んでも、私にはMCPの良さは全然理解できませんでした。なのに世の中はMCPブームと言っても良いぐらいMCPが流行っています。私が理解できないだけで、沢山の方がその良さを見出しています。これはまずいと思いました。私は何かを誤解してしまったのだと思い、色々と作り散らかし、同僚とディスカッションし続けてようやくMCPの有用性を腑に落ちるレベルまで理解することができました。

 理解できなかった理由はいくつか思い当たるのですが、とにかくMCPの良さをちゃんと理解できるまで物凄く遠回りをしました。そして理解した今ならば、私ならこうやって説明するなーというのが自分の中で湧き上がりました。

 この記事は私のようなMCPの有用性が理解できなかった人に向けてのMCP再入門しませんか、という意図で書こうと思いました。参考になればとても嬉しいです。

どの立ち位置からMCPの有用性を把握したのいか

 まず、MCPを理解するスタンスとして、AIアプリケーションを作成するに当たってMCPはどう便利なのかを知りたい、というモチベーションが私にはありました。なので同じスタンスの方にはこの記事は有用だと思います。Claude DesktopやVSCodeなどの第三者が作成したアプリケーションにおけるMCPの活用についての理解したい、という方には内容が濃すぎると思います。

 ここでいうAIアプリケーションとは、生成AIと通信しているアプリケーションのことを指します。AIアプリケーションはWebアプリ、WebAPI、スタンドアロンなど様々なものがあると思いますが、それらのAIアプリケーションでは生成AIと通信している前提です。例えば社内向けChatbotをWebアプリケーションとして提供している会社も多いと思いますが、社内向けChatbotもAIアプリケーションです。

MCPを知っておくべき最低限の用語

まずは最低限の用語の説明からします。私が思うMCP関連の最低限の用語は3つしかありません。

  1. MCP Client
  2. MCP Server
  3. Tool

あと、接続方式としてSTDIOとHTTP+SSE(or Streaming)があります。接続方式についてはMCPについての理解が進むとわかってきます。では1つずつご紹介します。

1. MCP Client

 MCP Serverと通信するための実装をしてあるAIアプリケーションのことで、ご自身で作成するAIアプリケーションのことだと理解すればOKです。

2. MCP Server

 Server、と名称から物理的なサーバーにデプロイして、そこへMCP Clientがアクセスするのだな、と思ってしまいますがそうではありません。ここ、大事なポイントで誤解しやすいところなので注意しましょう。MCP Serverとは、「MCPを通じて、特定の機能を公開する軽量プログラム」 のことです。(公式ページ

原文

MCP Servers: Lightweight programs that each expose specific capabilities through the standardized Model Context Protocol

 なんだかピンとこないと思いますので、この後詳しくご説明します。とにかく今は、MCP ServerはMCP Clientとは別のサーバーにデプロイするわけではない、ということだけ覚えておいてください。

 MCP Serverはただのプログラムなのであらゆることが処理できます。DBにクエリを発行したり、ファイルサーバーを検索したり、GitHubにIssueを作成したりできます。それら1つずつをメソッドとして実装し、MCPを使って外部に公開します。MCP Clientは公開されたメソッドのうち、使いたいものを呼び出して使用します。

正確には別サーバーにデプロイするMCP Serverもあります。が、それはMCPの理解が進んでから考えないとMCPを理解する遠回りになります。

3.Tool

 MCP Serverが公開するメソッドのことをMCPではToolと呼びます。MCP ClientはMCP ServerがどんなToolを持っているのかを知りません。そこでToolには自身がどんな処理をするToolなのかを解説するメタ情報を付与しておきます。MCP ClientがToolの一覧をMCP Serverに問い合わせると、MCP Serverから返却されたToolの一覧情報の中に各Toolのメタ情報が含まれているので、MCP Clientは適切なToolを選択できるようになります。メタ情報の付与についてはこの後のコードサンプルを見ていただければとても簡単なことがわかります。

接続方式

 MCP ClientとMCP Serverとの間の接続方式はSTDIOとHTTP+SSE(or Streaming)の2種類が用意されています。まずはSTDIOから理解することを強くお勧めします。というかSTDIOから理解しないとMCPへの理解の遠回りになります。STDIO方式の通信についてはこの後ご紹介します。

MCPの概念図

今までの説明を図にするとこんな感じです。公式のものと比べるとだいぶざっくりしていると思いますが、まずはこれで良いのです。理解が進むともうちょっと細かくなっていきます。

image.png

具体例からMCPを理解する

 私の結論として、まずはClaude DesktopとMCPとの組み合わせからMCPを理解するのが近道です。じゃあ仕方ないのでまずはClaude Desktopをインストールしようか、とやっても良いのですが、私はClaude Desktopをインストールしませんでした。しなくても大丈夫です。Claude Desktopのダウンロードページを見たり、Webで検索した結果からするに、おそらくChatGPTのようなChatbotのデスクトップ版のようなものなのだな、と理解できます。これだけで理解としては十分です。

Claude Desktop

image.png

 そしてClaude DesktopにMCPを拡張機能として設定することで、MCPを通じて様々な情報を得た結果をLLM(Claude)に渡したり、与えられたプロンプトによってはMCP越しに何かしらの処理を実行させることができるようになります。

Claude DesktopへのMCP ServerのセットアップからMCPを理解する

 Claude DesktopへのMCP Serverの設定例を見てみましょう。MCP公式ページのClaude DesktopへのMCP Serverセットアップをするチュートリアルを見ると、json形式の設定ファイルに対して、次のようなセットアップを行うことが記載されています。

claude_desktop_config.json
{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "C:\\Users\\username\\Desktop",
        "C:\\Users\\username\\Downloads"
      ]
    }
  }
}

 この例はFileSystem MCP ServerというMCP公式ページでも紹介されているMCP Serverを設定しています。このMCP Serverを設定すると、ファイルの読み書きやフォルダの作成・削除、ファイルの検索もできるようになるそうです。

 この設定ファイルから重要なことがいくつかわかります。まず、commandとしてnpxが使用されています。npxはNode.jsをインストールするとついてくるnpmというツールのコマンドのシンタックスシュガーで、Node.js製のツールを自動的にダウンロードして実行するツールです。そしてargsにある@modelcontextprotocol/server-filesystem、これがNode.jsで作られたMCP Serverです。残りの2つのファイルパスはこのMCP Serverへ渡す引数です。

 正確を記すためにnpxについて長々と書きましたが、要するにClaude DesktopはNode.js製のアプリケーションを起動している、ということがわかったのです。つまりMCP ServerはMCP Clientと同じマシンの中で稼働するプログラムに過ぎないのです。

 ここでMCP Serverの公式定義を思い出してみましょう。MCP Serverとは

 MCPを通じて特定の機能を公開する軽量プログラム

でした。なるほど、確かにMCP Serverはただのプログラムです。そしてこの設定を見る限り、MCP Serverは同一マシン内で起動したプロセスであり、MCP Client(Claude Desktop)とMCP Serverはプロセス間でデータのやり取りをしている訳です。

image.png

 そしてこのようにMCP ClientとMCP Serverが同一マシン内でプロセス間通信を行う際に、それぞれの標準入出力を使用して通信をする方式をSTDIO、とMCPでは呼んでいます。逆にいうと、STDIO方式を採用した場合はMCP ClientとMCP Serverは必ず同一マシン内に別プロセスとして存在します。

 詳しい方はご存知だと思いますが、標準入出力を使用してプロセス間通信を行いたい場合、親プロセスからもう1つのアプリケーションを子プロセスとして起動すると標準入出力の接続が簡単にできます。なので、Claude Desktopもこれに倣ってMCP Serverを自分で子プロセスとして立ち上げる方式をとっていると思われます。

image.png

 この例ではMCP ServerはNode.js製のアプリケーションとして実装されているのでnpxコマンドを使用しました。でも、コマンドを叩いてプロセスを起動しているわけですから、MCP ServerはNode.jsアプリである必要はありません。Python、.NET、Go、SDKさえあれば何でも良い訳です。

 実際、Webコンテンツを取得するMCP ServerであるfetchはPython製なので、npxではなくuvxコマンドを使用するように設定します。

MCP Serverとは結局何なのか

 Claude DesktopとMCP Serverのセットアップからわかったことは、MCP Serverはただのプログラムでした。しかもMCP Client(Claude Desktop)から同一マシン内で起動する子プロセスです。

 結局MCP Serverとはなんでしょうか。MCP Serverが公開するメソッド(Tool)を使用するとデータを取得したり、何か処理をさせることが可能な訳ですが、これは要するにAI Agentのことだと理解すれば良いでしょう。

 AI Agentの明確な定義をどこかの団体がしてくれている訳ではない(と思う)のですが、要するにデータを取得したり、何か処理をしてくれる独立した「何か」です。「何か」は製品やSDKによって変わります。例えば

  • OpenAI Agent SDKの場合、Agentはオブジェクト
  • Azure AI Agent Serviceの場合、Agentはサービス

です。

そして

 MCP Serverの場合、Agentはプロセス

と理解すれば良いことがわかります。

まとめると、

MCP ServerとはMCP Clientと同一マシン内で別プロセスとして稼働するAgentである

となります。これをServerと呼ぶのはやめてほしかったですね。
※追記:Serverという言葉の定義からするとMCP Serverという名前付けが間違っているわけではありません。ご質問をいただいたのでこの一文の意図についてはコメントに記載してありますが、完全に余計な一言でしたので削除します。

MCP の実装の難易度はどうか

 さてMCPの本質はもう掴んでいます。次に気になるのはMCPの実装難易度です。これが高い場合は採用を見送ることになるでしょう。実際に実装を見てみます。

超シンプルなMCP Serverを実装する

MCP Clientから先に作りたい気持ちがあると思いますが、適当なMCP Serverがないのでまずは超シンプルなMCP Serverを作ります。

Python

適当にMCP Server用のフォルダを作成して移動します。

uv init
uv add mcp[cli]
main.py
from mcp.server.fastmcp import FastMCP

# Initialize FastMCP server
mcp = FastMCP("weather")

@mcp.tool()
async def get_alerts(prefecture: str) -> str:
    """Get weather alerts for a Japan prefecture.

    Args:
        prefecture: Japan prefecture name (e.g. Tokyo, Osaka)
    """

    return "現在警報はありません"

@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
    """Get weather forecast for a location.

    Args:
        latitude: Latitude of the location
        longitude: Longitude of the location
    """

    return "東京は雨です"

if __name__ == "__main__":
    # Initialize and run the server
    mcp.run(transport='stdio')

 get_alertsとget_forecastの2つのToolを公開するシンプルなMCP Serverです。MCP Clientから問い合わせると、どんなToolをMCP Serverが持っているのかがわかるので、MCP Clientは取得したTool群のうち、プロンプトにとって適切なものを実行することになります。
 一番下にtransport='stdio'となっていますが、これはMCP Clientとのデータのやり取りに標準入出力を使用することを意味しています。

 このMCP Serverをuv run main.pyと動かしても何も起こりません。子プロセスとして立ち上げて標準入出力を接続するMCP Clientが必要だからです。そこで、公式に用意されているテスト用のMCP Clientである@modelcontextprotocol/inspectorを使います。

 使い方は簡単です。ローカル環境にNode.jsが入っていれば、次のコマンドを叩くだけです。ただし、今作ったばかりのMCP Serverのルートディレクトリでコマンドを叩く必要があることに注意してください。

npx @modelcontextprotocol/inspector

 npx コマンドはローカルにモジュールがなかったら一時的にモジュールをダウンロードして実行してくれます。

npxでダウンロードしたモジュールの保存先は
Mac: ~/.npm/_npx
Windows: %LocalAppData%/npm-cache/_npx
だそうです。正確には

npm config get cache

でキャッシュのパスを得ると良さそうです。ダウンロードしたモジュールを削除するにはこのパスの中の_npxフォルダを削除する必要があるようですが、最新のnpm@11ではnpxに対するコマンドが用意されているようなので、そちらを使用して消した方が良さそうです。

Inspectorを起動すると、次のようなログがコンソールに表示されるはずです。

image.png

 真ん中あたりに表示されたhttp://127.0.0.1:6274 をブラウザで開きます。次にcommandにuv、Arugumentsにrun main.pyと入力してConnectボタンをクリックします。

image.png

すると、ボタンの下にConnectedと表示されるはずです。次に画面上部のToolsをクリックします。

image.png

List Toolsをクリックします。

image.png

すると、先ほどMCP Serverで実装した2つのメソッドget_alertsとget_forecastが表示されます。試しにget_alertsをクリックしてみましょう。

image.png

すると右側にget_alertsの説明と引数のprefectureを入力するテキストボックスが表示されます。MCP Serverで実装した通りです。試しにTokyoと入れてRun Toolをクリックしてみましょう。

image.png

固定値の"現在警報はありません"が表示されます。

image.png

これで超シンプルなMCP Serverが問題なく実装できていることが確認できました。

超シンプルなMCP Clientを実装する

 では、自前のAIアプリケーションをMCP ClientとしてMCP Serverを使うようにする実装をしましょう。ただ単にMCP Serverのメソッドを叩くこともできるのですが、それだとあまり意味はありません。私たちはAIアプリケーションにMCPを組み込みたいのです。なのでやりたいことは次の流れです。

  1. ユーザーからプロンプトを受け取る
  2. MCP Serverからtoolの一覧を受け取る
  3. 生成AIのモデルを使って応答生成に必要なtoolをFunction Callingで特定
  4. MCP Serverに引数を渡して処理を実行、結果を受け取る
  5. 受け取った結果を使って生成AIで応答を生成し、返却

 Function Callingとは、複数のメソッドの情報(何ができるメソッドなのか)を生成AIに渡して、処理すべきメソッドを判定させる仕組みのことです。メソッドはAIアプリ側にあるので生成AIがメソッドの実行まではしてくれません。

 これらの処理を自動化する場合はSemantic KernelやLangChainなどのフレームワークを使用するか、Agent用のSDKを使用します。ここではMCPに対応しているSDKの実装例としてOpenAI Agent SDKを使用する場合ご紹介します。

OpenAI Agent SDK

Azure上にデプロイしたOpenAI GPT-4oを使用します。

uv init
uv sync
uv add openai-agents

まずはMCP Serverを使用しないで単純にGPT-4oと通信するだけの実装をします。

main.py
from openai import AsyncAzureOpenAI
from openai.types.responses import ResponseTextDeltaEvent
from agents import set_default_openai_client, Agent, Runner
import asyncio

api_version = "2025-03-01-preview"
endpoint = "YOUR_OPENAI_ENDPOINT"
apikey = "YOUR_OPENAI_APIKEY"

async def main():
    openai_client = AsyncAzureOpenAI(
        api_key=apikey,
        api_version=api_version,
        azure_endpoint=endpoint,
    )

    # Set the default OpenAI client for the Agents SDK
    # set trace off because of recomendation from OpenAI
    set_default_openai_client(openai_client, use_for_tracing=False)

    try:

        agent = Agent(
            name="Assistant",
            instructions="You are a helpful assistant.",
            model="gpt-4o"
        )

        result = Runner.run_streamed(agent, "東京に行きます。天気予報は?")

        async for event in result.stream_events():
            if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent):
                print(event.data.delta, end="", flush=True)
    finally:
        await openai_client.close()

if __name__ == "__main__":
    asyncio.run(main())

結果はこんな感じです。

image.png

では、このコードを次のように修正して、先ほど作った超シンプルMCP Serverを使用してみましょう。

main.py
from openai import AsyncAzureOpenAI
from openai.types.responses import ResponseTextDeltaEvent
from agents import set_default_openai_client, Agent, Runner
import asyncio
+from agents.mcp import MCPServerStdio

api_version = "2025-03-01-preview"
endpoint = "YOUR_OPENAI_ENDPOINT"
apikey = "YOUR_OPENAI_APIKEY"

async def main():
    openai_client = AsyncAzureOpenAI(
        api_key=apikey,
        api_version=api_version,
        azure_endpoint=endpoint,
    )

    # Set the default OpenAI client for the Agents SDK
    # set trace off because of recomendation from OpenAI
    set_default_openai_client(openai_client, use_for_tracing=False)

+    server = MCPServerStdio(
+        name="Weather Server, via uv",
+        params={
+            "command": "uv",
+            "args": ["run", "YOUR/UltraSimpleMCPSErver/PATH/main.py"],
+        },
+    )

    try:
+       await server.connect()

        agent = Agent(
            name="Assistant",
            instructions="You are a helpful assistant.",
+           mcp_servers=[server],
            model="gpt-4o"
        )

        result = Runner.run_streamed(agent, "東京に行きます。天気予報は?")

        async for event in result.stream_events():
            if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent):
                print(event.data.delta, end="", flush=True)
    finally:
+       await server.cleanup()
        await openai_client.close()

if __name__ == "__main__":
    asyncio.run(main())

argsの"YOUR/UltraSimpleMCPSErver/PATH/main.py"ですが、これはご自身が作った超シンプルMCP Serverのルートディレクトリパス/main.pyに変更してください。相対パスでも多分大丈夫です。

実行すると結果はこのようになります。

image.png

ちゃんとMCP Serverを使ってくれましたね。

結局MCPは有効なのだろうか

 そろそろ考察しましょう。AIアプリケーション(MCP Client)がMCP Server(という小さなプログラム)を同一マシンの別プロセスとして呼び出すことで、AIアプリケーションを実に簡単に、しかも大幅に拡張できることがわかりました。

 まずわかっているのはClaude DesktopのようなAIデスクトップアプリケーションをユーザーが自分で拡張できるようになります。VS CodeもGitHub CopilotのAgentモードでMCP Serverを組み込む機能をPreviewリリースしました。AgentモードでLLMにあれこれ指示を出すと、MCP Serverをよしなに使って様々な処理をやってくれます。

 例えば

  • GitHubにリポジトリ作って / Issue作って / コミット&プッシュして
  • PostgreSQLのこのテーブルのスキーマを見せて / ダミーデータを作って
  • FigmaのこのデザインをReactで実装して

などがVS Codeで可能になる訳です。

 拡張性を見込めるのはデスクトップアプリだけではないでしょう。生成AI(の使用)を組み込んだWebアプリケーションの場合もとても簡単に機能拡張できるようになります。例えばCopilot Studioでは事前にMicrosoftが用意したMCP Serverを選ぶことで簡単に生成AIチャットを機能拡張できるようになりました。

 もっと身近な例で考えてみます。社内向けの生成AIのChatbotがあなたの会社にはありませんか?自社で生成AIのChatbotを構築しているなら、MCP Client対応すれば様々な機能拡張が簡単にできるようになる、ということです。夢が広がりますね。

 ポイントは既に沢山の便利そうなMCP Serverが公開してある、という点です。それらをダウンロードするだけで、すぐにアプリケーションに組み込んで機能拡張できるのです。これはものすごく大きなメリットです。どうしても必要な機能を持つMCP Serverがない場合のみ、自分で作成すれば良いのです。

 MCPの有用性は確かに高いと思います。この汎用性を活かすならRAGもMCP Serverで作り直してToolの1つにしてしまった方が良いでしょう。

リモートMCP Serverは有用なのか

 もう1つ考察しておきましょう。MCP Serverとの接続方式にはSTDIO以外にもう1つ、HTTP+SSE(or Streaming)というものがあります。その名前まま、HTTPとServerSendEventによる接続です。この接続方式を採用すれば、MCP ServerをMCP Clientの外部のサーバーにデプロイしてあっても接続することができます。

 このリモートMCP Serverに有用性はあるのでしょうか。例えばWebAPIのエンドポイントを公開しているSaaS製品がリモートMCP Serverを公開する価値はあるでしょうか。今のところ、その価値はないでしょう。GitHubのMCP Serverを見てみると、STDIOでの接続になっています。ローカルマシンのMCP ServerからGitHubのWebAPIを叩けば良いのだからリモートMCP Serverを用意する必要性がどこにもありません。APIKEYやTokenをローカルに格納できるというセキュリティ面でのメリットもあります。

 もしリモートMCP Serverが活躍するシナリオがあるとしたら、MCP Server越しにしかリモート環境内部のリソースにアクセスできないような場合かもしれません。でも私には具体例が思いつきませんでした。

最後に

 いかがだったでしょうか。MCPの有用性が腑に落ちていただけたなら嬉しいです。それにしてもOpenAI Agent SDKしか試していませんが、AIアプリケーションへの組み込みが驚くほど簡単だと思いませんか?本当にすごいですね。他にもSemantic KernelやLangChain、AutoGenなどもMCP対応が行われています。是非試して記事にしてください。読みたいです。長文記事を最後までお読みいただきまして、ありがとうございました。

Appendix: HTTP+SSE方式の実装

HTTP+SSE方式はあまり出番がないと思いますが、一応サンプル実装はしてみましたので残しておきます。

MCP Server

HTTP+SSEの通信方式はStreamingに取って代わることが決定しているようですが、SDKが準備されるまでは少し時間がかかりそうです。

Python

mcp[cli] ver:1.6.0
Starlette ver:0.46.1
uvicorn ver:0.34.0

requirements.txt
mcp[cli]
Starlette
uvicorn
pip install -r requirements.txt
server.py
from mcp.server.fastmcp import FastMCP
from starlette.applications import Starlette
from starlette.routing import Mount

mcp = FastMCP("Echo")

@mcp.resource("echo://{message}")
def echo_resource(message: str) -> str:
    """Echo a message as a resource"""
    return f"Resource echo: {message}"

@mcp.tool()
def echo_tool(message: str) -> str:
    """Echo a message as a tool"""
    return f"Tool echo: {message}"

@mcp.prompt()
def echo_prompt(message: str) -> str:
    """Create an echo prompt"""
    return f"Please process this message: {message}"

# Starletteアプリにマウント
app = Starlette(
    routes=[
        Mount('/', app=mcp.sse_app()),
    ]
)

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="localhost", port=3000)

MCP Client

python

mcp ver:1.6.0
asyncio ver:3.4.3

requirements.txt
mcp
asyncio
pip install -r requirements.txt
client.py
from mcp import ClientSession
from mcp.client.sse import sse_client

async def main():
    async with sse_client("http://localhost:3000/sse") as streams:
        async with ClientSession(streams[0], streams[1]) as session:
            await session.initialize()

            # ツール一覧取得
            listTools = await session.list_tools()
            print("Available tools:")
            for tool in listTools.tools:
                print(f" - {tool.name}, Description: {tool.description}, Schema: {tool.model_json_schema()}")

            # Echo ツールを取得
            echo_tool = next((tool for tool in listTools.tools if tool.name == "echo_tool"), None)

            # Echo ツールを使う
            response = await session.call_tool(
                name=echo_tool.name,
                arguments={"message": "Hello, world!"})

            print("")            
            print("Echo response:", response.content)

if __name__ == "__main__":
    import asyncio
    asyncio.run(main())
C#

ModelContextProtocol ver:0.1.0-preview.7

dotnet add package ModelContextProtocol
Progiram.cs
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol.Transport;

await using var client = await McpClientFactory.CreateAsync(
    clientTransport: new SseClientTransport(
        new SseClientTransportOptions()
        {
            Endpoint = new Uri("http://localhost:3000/sse"),
        })
);

var tools = await client.ListToolsAsync();

foreach (var tool in tools)
{
    Console.WriteLine($"{tool.Name}, Description: {tool.Description}, Schema: {tool.JsonSchema}");
}

// Echo ツールを取得
var echoTool = tools.FirstOrDefault(t => t.Name == "echo_tool")
                             ?? throw new InvalidOperationException();

// Echo ツールを実行
var response = await client.CallToolAsync(echoTool.Name, new Dictionary<string, object?>
{
    ["message"] = "Hello!",
});

// レスポンスを表示
foreach (var content in response.Content)
{
    Console.WriteLine($"{content.Type}, {content.Text}");
}
168
150
6

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
168
150

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?