1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Django-Ninja x MCP:1つのDjangoサーバーでWeb・REST API・MCPサービスを一括ホストする(独自実装版)

1
Posted at

近年、LLM(大規模言語モデル)に外部のデータベースやAPIツールを統合するための統一規格として MCP(Model Context Protocol) が急速に広まっています。AIエージェントに自社サービスの機能を提供したい場合、MCPサーバーの構築は非常に強力な選択肢です。

しかし、「MCP用にわざわざ別サーバーを立てたり、APIロジックを二重で管理したりしたくない……」と感じる方も多いのではないでしょうか?

本記事では、Pythonのモダンで高速なAPIフレームワークである Django-Ninja をベースに、単一のDjangoサーバー上で「Webサービス」「REST API」「MCPサービス」をまとめてホストする構成 を紹介します。既存のDjango APIエンドポイントをそのままMCPツールとして活用できるため、エージェント連携のオーバーヘッドを劇的に削減できます。

利用ソフトウェアのバージョン

  • uv: 0.12.3
  • mcp: 2.0.0
  • django: 6.0.8
  • django-ninja: 1.6.2
  • Bionic(クライアントとして確認用): 1.0.6+5

サンプルサーバー構築

以下の手順は、macOSで確認しています。

環境構築

uvでsampleというフォルダーを作りDjangoの環境構築をします。

uv init --no-package -p 3.14 sample
cd sample
mkdir src
rm main.py
uv add mcp==2.0.0 django==6.0.8 django-ninja==1.6.2

プロジェクトとアプリの作成

以下のようにしてプロジェクトとアプリを作成します。

uv run django-admin startproject project src
uv run --directory src manage.py startapp core

実装

ここでは、weatherというサービスを実装します。

src/project/settings.py抜粋

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "ninja",
    "core.apps.CoreConfig",
]

ASGI_APPLICATION = "project.asgi.application"

src/project/asgi.py

import os

# djangoをインポートする前に設定しないといけない
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")

from contextlib import asynccontextmanager

from django.core.asgi import get_asgi_application
from starlette.applications import Starlette
from starlette.routing import Mount

from core.api import api

from .mcp import mcp_server, register_ninja_to_mcp


@asynccontextmanager
async def lifespan(app):
    # mounted sub-appのlifespanは自動実行されないため、host app側で起動する
    async with mcp_server.session_manager.run():
        yield


register_ninja_to_mcp(api, mcp_server)
django_app = get_asgi_application()
mcp_app = mcp_server.streamable_http_app(streamable_http_path="/")

application = Starlette(
    routes=[
        Mount("/api/mcp", app=mcp_app),
        Mount("/", app=django_app),
    ],
    lifespan=lifespan,
)

src/project/mcp.py

import inspect

from mcp.server import MCPServer
from ninja import NinjaAPI

api = NinjaAPI()
mcp_server = MCPServer(name="Sample API", description="API with MCP integration")


def register_ninja_to_mcp(api: NinjaAPI, mcp_server: MCPServer) -> None:
    """NinjaAPI に登録されているエンドポイントをMCPServer の Tool として登録"""
    for _, router in api._routers:
        for path_view in router.path_operations.values():
            for op in path_view.operations:
                func = op.view_func

                # 1. 元の関数のシグネチャから 'request', 'req' 引数を取り除く
                sig = inspect.signature(func)
                new_params = [p for p in sig.parameters.values() if p.name not in ("request", "req")]
                new_sig = sig.replace(parameters=new_params)

                # 2. 型ヒント (__annotations__) からも 'request', 'req' を取り除く
                orig_annotations = getattr(func, "__annotations__", {})
                new_annotations = {k: v for k, v in orig_annotations.items() if k not in ("request", "req")}

                # 3. async / sync 両対応のラッパー関数を生成
                def make_wrapper(target_func, target_sig, target_annotations):
                    if inspect.iscoroutinefunction(target_func):

                        async def wrapper(*args, **kwargs):
                            return await target_func(None, *args, **kwargs)

                    else:

                        def wrapper(*args, **kwargs):
                            return target_func(None, *args, **kwargs)

                    # メタデータ・シグネチャ・型ヒントを引き継ぐ
                    wrapper.__name__ = target_func.__name__
                    wrapper.__doc__ = target_func.__doc__
                    wrapper.__module__ = target_func.__module__
                    wrapper.__signature__ = target_sig
                    wrapper.__annotations__ = target_annotations
                    return wrapper

                mcp_server.add_tool(make_wrapper(func, new_sig, new_annotations))

src/project/urls.py

from django.contrib import admin
from django.urls import path

from core.api import api

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/", api.urls),
]

src/core/api.py

from project.mcp import api


@api.get("/weather/{city}")
def weather(request, city: str) -> dict[str, str]:
    """都市の天気"""
    return {"message": f"{city}の天気は曇りのち晴れ"}

実行

マイグレーションを実行し、サーバーを起動します。

uv run --directory src manage.py migrate
uv run --directory src uvicorn project.asgi:application --reload --lifespan on

注意
manage.py runserverではエラーになります。

Web確認

http://localhost:8000/adminでWebを確認できます。以下でユーザーを作成するとログインできます。

uv run --directory src manage.py createsuperuser

API確認

http://localhost:8000/api/docsでAPIを確認できます。

注意
http://localhost:8000/api/docs/は404になります。最後のスラッシュは取ってください。

MCP確認

ここではBionicで確認します。以下からダウンロードしてインストールできます。

MCP設定

Bionicを起動したら設定画面(⌘+カンマ)を開き、左のタブの「Connected Apps」を選んでください。
右下の「Add custom MCP」で開く画面で以下を設定し、「Add MCP」を押してください。

注意
http://localhost:8000/api/mcpは404になります。最後のスラッシュは付けてください。

確認

左上の「Back to app」で戻り、チャット画面を開いてください。

「東京の天気」を入力してください。
「東京の天気は曇りのち晴れです。」と帰ってきたら成功です。

参考

下記は django-ninja-mcp を使った紹介記事です。本記事よりシンプルですが、django-ninja-mcp に依存するため最新版が使えないことがあります。

1
0
0

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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?