diff --git a/dribdat/app.py b/dribdat/app.py
index 7adcd51b..5ea28b65 100644
--- a/dribdat/app.py
+++ b/dribdat/app.py
@@ -98,6 +98,7 @@ def init_talisman(app):
def register_blueprints(app):
"""Register Flask blueprints."""
app.register_blueprint(public.api.blueprint)
+ app.register_blueprint(public.mcp.blueprint)
app.register_blueprint(public.auth.blueprint)
app.register_blueprint(public.views.blueprint)
app.register_blueprint(public.feeds.blueprint)
diff --git a/dribdat/public/__init__.py b/dribdat/public/__init__.py
index 188db61b..a2266b78 100644
--- a/dribdat/public/__init__.py
+++ b/dribdat/public/__init__.py
@@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
"""The public module, including the homepage, API and user auth."""
-from . import views, project, auth, api, feeds # noqa
+from . import views, project, auth, api, feeds, mcp # noqa
diff --git a/dribdat/public/mcp.py b/dribdat/public/mcp.py
new file mode 100644
index 00000000..9466a8ce
--- /dev/null
+++ b/dribdat/public/mcp.py
@@ -0,0 +1,231 @@
+import uuid
+import json
+import asyncio
+import logging
+import time
+from flask import (
+ Blueprint,
+ render_template,
+ redirect,
+ url_for,
+ request,
+ flash,
+ jsonify,
+ current_app,
+ Response,
+ stream_with_context
+)
+from flask_login import login_required, current_user
+from mcp.server.fastmcp import FastMCP
+from mcp.shared.exceptions import McpError
+import mcp.types as types
+from ..user.models import Event, Project, Activity, User
+from ..database import db
+from ..utils import markdownit
+from ..public.projhelper import project_action
+from ..user.constants import PR_CHALLENGE
+from ..user import stageProjectToNext
+
+blueprint = Blueprint("mcp", __name__)
+
+# Initialize FastMCP server
+mcp_server = FastMCP("Dribdat")
+
+# --- MCP Tools ---
+
+@mcp_server.tool()
+def get_event_info(event_id: int = None):
+ """Get information about a specific event or the current one."""
+ if event_id:
+ event = Event.query.get(event_id)
+ else:
+ event = Event.query.filter_by(is_current=True).first() or \
+ Event.query.order_by(Event.id.desc()).first()
+
+ if not event:
+ return "No event found."
+
+ return json.dumps(event.get_full_data(), default=str)
+
+@mcp_server.tool()
+def search_projects(query: str, event_id: int = None):
+ """Search for projects matching a query string."""
+ q = Project.query.filter(Project.is_hidden == False)
+ if event_id:
+ q = q.filter_by(event_id=event_id)
+
+ search_query = f"%{query}%"
+ projects = q.filter(
+ (Project.name.ilike(search_query)) |
+ (Project.summary.ilike(search_query)) |
+ (Project.longtext.ilike(search_query))
+ ).all()
+
+ return json.dumps([p.data for p in projects], default=str)
+
+@mcp_server.tool()
+def get_project_details(project_id: int):
+ """Get full details of a project by ID."""
+ project = Project.query.get(project_id)
+ if not project:
+ return "Project not found."
+
+ data = project.data
+ data['longtext'] = project.longtext
+ data['autotext'] = project.autotext
+ return json.dumps(data, default=str)
+
+@mcp_server.tool()
+def get_activities(project_id: int = None, limit: int = 10):
+ """Get recent activities/posts, optionally filtered by project."""
+ q = Activity.query
+ if project_id:
+ q = q.filter_by(project_id=project_id)
+
+ activities = q.order_by(Activity.timestamp.desc()).limit(limit).all()
+ return json.dumps([a.data for a in activities], default=str)
+
+@mcp_server.tool()
+def add_post(project_id: int, text: str):
+ """Add a new post/update to a project."""
+ # Note: Authentication context is handled via the transport/token
+ user = getattr(request, 'mcp_user', None)
+ if not user:
+ return "Error: MCP session not authenticated."
+
+ project = Project.query.get(project_id)
+ if not project:
+ return "Error: Project not found."
+
+ project_action(project.id, "update", action="post", text=text, for_user=user)
+ return f"Post added to project '{project.name}'."
+
+@mcp_server.tool()
+def update_project_status(project_id: int):
+ """Promote a project to the next stage/level."""
+ user = getattr(request, 'mcp_user', None)
+ if not user:
+ return "Error: MCP session not authenticated."
+
+ project = Project.query.get(project_id)
+ if not project:
+ return "Error: Project not found."
+
+ # Check if user is allowed to edit
+ if not user.is_admin and project.user_id != user.id:
+ # Check if user has starred (joined) the project
+ starred = Activity.query.filter_by(
+ name='star', project_id=project.id, user_id=user.id
+ ).first()
+ if not starred:
+ return "Error: You do not have permission to update this project."
+
+ if stageProjectToNext(project):
+ project.update_now()
+ db.session.add(project)
+ db.session.commit()
+ return f"Project '{project.name}' promoted to stage '{project.phase}'."
+ else:
+ return f"Project '{project.name}' is already at the maximum stage or not ready for promotion."
+
+# --- MCP Blueprint Routes ---
+
+@blueprint.route("/mcp/auth")
+@login_required
+def mcp_auth():
+ """Page for user to get their one-time MCP token."""
+ if not current_user.mcp_token:
+ current_user.mcp_token = str(uuid.uuid4())
+ db.session.add(current_user)
+ db.session.commit()
+ return render_template("public/mcp_auth.html", token=current_user.mcp_token)
+
+@blueprint.route("/api/mcp/sse")
+def mcp_sse():
+ """SSE endpoint for MCP."""
+ token = request.args.get("token")
+ user = User.query.filter_by(mcp_token=token).first()
+ if not user:
+ return "Unauthorized", 401
+
+ def event_stream():
+ # First message is the endpoint for client-to-server messages
+ msg_url = url_for(".mcp_messages", token=token, _external=True)
+ yield f"event: endpoint\ndata: {msg_url}\n\n"
+
+ while True:
+ time.sleep(30)
+ yield ": keep-alive\n\n"
+
+ return Response(stream_with_context(event_stream()), mimetype="text/event-stream")
+
+@blueprint.route("/api/mcp/messages", methods=["POST"])
+def mcp_messages():
+ """Endpoint for client-to-server messages."""
+ token = request.args.get("token")
+ user = User.query.filter_by(mcp_token=token).first()
+ if not user:
+ return "Unauthorized", 401
+
+ # Attach user to request for tools to use
+ request.mcp_user = user
+
+ payload = request.get_json()
+ method = payload.get('method')
+ params = payload.get('params', {})
+ request_id = payload.get('id')
+
+ try:
+ if method == "initialize":
+ result = {
+ "protocolVersion": "2024-11-05",
+ "capabilities": {
+ "tools": {}
+ },
+ "serverInfo": {
+ "name": "Dribdat",
+ "version": "0.1.0"
+ }
+ }
+ return jsonify({"jsonrpc": "2.0", "id": request_id, "result": result})
+
+ elif method == "tools/list":
+ tools_list = []
+ for name, tool in mcp_server._tool_manager.list_tools():
+ tools_list.append({
+ "name": name,
+ "description": tool.description,
+ "inputSchema": tool.parameters.model_json_schema()
+ })
+ return jsonify({"jsonrpc": "2.0", "id": request_id, "result": {"tools": tools_list}})
+
+ elif method == "tools/call":
+ tool_name = params.get('name')
+ tool_args = params.get('arguments', {})
+
+ # Lookup the tool and call it
+ tool = next((t for n, t in mcp_server._tool_manager.list_tools() if n == tool_name), None)
+ if not tool:
+ return jsonify({"jsonrpc": "2.0", "id": request_id, "error": {"code": -32601, "message": "Tool not found"}})
+
+ # Execute tool (FastMCP tools can be sync or async)
+ import inspect
+ if inspect.iscoroutinefunction(tool.fn):
+ loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(loop)
+ res_content = loop.run_until_complete(tool.fn(**tool_args))
+ else:
+ res_content = tool.fn(**tool_args)
+
+ return jsonify({
+ "jsonrpc": "2.0",
+ "id": request_id,
+ "result": {
+ "content": [{"type": "text", "text": str(res_content)}]
+ }
+ })
+
+ return jsonify({"jsonrpc": "2.0", "id": request_id, "error": {"code": -32601, "message": "Method not found"}})
+ except Exception as e:
+ current_app.logger.error(f"MCP Error: {str(e)}")
+ return jsonify({"jsonrpc": "2.0", "id": request_id, "error": {"code": -32603, "message": str(e)}})
diff --git a/dribdat/templates/public/mcp_auth.html b/dribdat/templates/public/mcp_auth.html
new file mode 100644
index 00000000..033bcb12
--- /dev/null
+++ b/dribdat/templates/public/mcp_auth.html
@@ -0,0 +1,54 @@
+{% extends "layout.html" %}
+
+{% block page_title %}MCP Authentication{% endblock %}
+
+{% block content %}
+
+
+
+
MCP Authentication
+
+ Use the following token to connect your Model Context Protocol (MCP) client to Dribdat.
+
+
+
+
+
Your Connection Token
+ {{ token }}
+
+
+
+
+
+
Connection Instructions
+
To use this server with an MCP client (like MCP Inspector or a custom agent), use the following SSE URL:
+
+ {{ url_for('mcp.mcp_sse', token=token, _external=True) }}
+
+
+
+ This token provides access to your account via MCP. Keep it secure and do not share it.
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/dribdat/templates/public/useredit.html b/dribdat/templates/public/useredit.html
index 3ed333bc..f0f18f24 100644
--- a/dribdat/templates/public/useredit.html
+++ b/dribdat/templates/public/useredit.html
@@ -33,6 +33,18 @@
{{ render_form(url_for('auth.user_profile'), form, formid='userEdit') }}
+
+
{% if user.sso_id %}
diff --git a/dribdat/user/models.py b/dribdat/user/models.py
index 41e3b91c..f013e70f 100644
--- a/dribdat/user/models.py
+++ b/dribdat/user/models.py
@@ -109,6 +109,7 @@ class User(UserMixin, PkModel):
sso_id = Column(db.String(128), nullable=True)
# A temporary hash for logins
hashword = Column(db.String(128), nullable=True)
+ mcp_token = Column(db.String(128), nullable=True)
updated_at = Column(db.DateTime(timezone=True), nullable=True, default=func.now())
# The hashed password
password = Column(db.String(128), nullable=True)
diff --git a/migrations/versions/b0bfebbaa9db_add_mcp_token_to_user.py b/migrations/versions/b0bfebbaa9db_add_mcp_token_to_user.py
new file mode 100644
index 00000000..eeea5f2d
--- /dev/null
+++ b/migrations/versions/b0bfebbaa9db_add_mcp_token_to_user.py
@@ -0,0 +1,32 @@
+"""Add mcp_token to User
+
+Revision ID: b0bfebbaa9db
+Revises: ac23802e6154
+Create Date: 2026-04-04 20:28:47.349111
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = 'b0bfebbaa9db'
+down_revision = 'ac23802e6154'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('users', schema=None) as batch_op:
+ batch_op.add_column(sa.Column('mcp_token', sa.String(length=128), nullable=True))
+
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('users', schema=None) as batch_op:
+ batch_op.drop_column('mcp_token')
+
+ # ### end Alembic commands ###
diff --git a/poetry.lock b/poetry.lock
index 839cdd42..558d500e 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -27,12 +27,32 @@ version = "0.7.0"
description = "Reusable constraint types to use with typing.Annotated"
optional = false
python-versions = ">=3.8"
-groups = ["dev"]
+groups = ["main", "dev"]
files = [
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
]
+[[package]]
+name = "anyio"
+version = "4.13.0"
+description = "High-level concurrency and networking framework on top of asyncio or Trio"
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708"},
+ {file = "anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc"},
+]
+
+[package.dependencies]
+exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""}
+idna = ">=2.8"
+typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
+
+[package.extras]
+trio = ["trio (>=0.32.0)"]
+
[[package]]
name = "async-timeout"
version = "5.0.1"
@@ -345,7 +365,7 @@ files = [
{file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"},
{file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"},
]
-markers = {main = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\"", dev = "platform_python_implementation != \"PyPy\""}
+markers = {main = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\" or platform_python_implementation != \"PyPy\"", dev = "platform_python_implementation != \"PyPy\""}
[package.dependencies]
pycparser = {version = "*", markers = "implementation_name != \"PyPy\""}
@@ -654,7 +674,7 @@ version = "46.0.6"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false
python-versions = "!=3.9.0,!=3.9.1,>=3.8"
-groups = ["dev"]
+groups = ["main", "dev"]
files = [
{file = "cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8"},
{file = "cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30"},
@@ -787,7 +807,7 @@ version = "1.3.1"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
-groups = ["dev"]
+groups = ["main", "dev"]
markers = "python_version == \"3.10\""
files = [
{file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"},
@@ -1515,6 +1535,18 @@ setproctitle = ["setproctitle"]
testing = ["coverage", "eventlet (>=0.40.3)", "gevent (>=24.10.1)", "h2 (>=4.1.0)", "httpx[http2]", "pytest", "pytest-asyncio", "pytest-cov", "uvloop (>=0.19.0)"]
tornado = ["tornado (>=6.5.0)"]
+[[package]]
+name = "h11"
+version = "0.16.0"
+description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"},
+ {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"},
+]
+
[[package]]
name = "hf-xet"
version = "1.4.2"
@@ -1745,6 +1777,65 @@ files = [
{file = "hiredis-2.4.0.tar.gz", hash = "sha256:90d7af678056c7889d86821344d79fec3932a6a1480ebba3d644cb29a3135348"},
]
+[[package]]
+name = "httpcore"
+version = "1.0.9"
+description = "A minimal low-level HTTP client."
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"},
+ {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"},
+]
+
+[package.dependencies]
+certifi = "*"
+h11 = ">=0.16"
+
+[package.extras]
+asyncio = ["anyio (>=4.0,<5.0)"]
+http2 = ["h2 (>=3,<5)"]
+socks = ["socksio (==1.*)"]
+trio = ["trio (>=0.22.0,<1.0)"]
+
+[[package]]
+name = "httpx"
+version = "0.28.1"
+description = "The next generation HTTP client."
+optional = false
+python-versions = ">=3.8"
+groups = ["main"]
+files = [
+ {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"},
+ {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"},
+]
+
+[package.dependencies]
+anyio = "*"
+certifi = "*"
+httpcore = "==1.*"
+idna = "*"
+
+[package.extras]
+brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""]
+cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
+http2 = ["h2 (>=3,<5)"]
+socks = ["socksio (==1.*)"]
+zstd = ["zstandard (>=0.18.0)"]
+
+[[package]]
+name = "httpx-sse"
+version = "0.4.3"
+description = "Consume Server-Sent Event (SSE) messages with HTTPX."
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc"},
+ {file = "httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d"},
+]
+
[[package]]
name = "huggingface-hub"
version = "0.35.3"
@@ -2281,6 +2372,39 @@ files = [
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
]
+[[package]]
+name = "mcp"
+version = "1.27.0"
+description = "Model Context Protocol SDK"
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "mcp-1.27.0-py3-none-any.whl", hash = "sha256:5ce1fa81614958e267b21fb2aa34e0aea8e2c6ede60d52aba45fd47246b4d741"},
+ {file = "mcp-1.27.0.tar.gz", hash = "sha256:d3dc35a7eec0d458c1da4976a48f982097ddaab87e278c5511d5a4a56e852b83"},
+]
+
+[package.dependencies]
+anyio = ">=4.5"
+httpx = ">=0.27.1"
+httpx-sse = ">=0.4"
+jsonschema = ">=4.20.0"
+pydantic = ">=2.11.0,<3.0.0"
+pydantic-settings = ">=2.5.2"
+pyjwt = {version = ">=2.10.1", extras = ["crypto"]}
+python-multipart = ">=0.0.9"
+pywin32 = {version = ">=310", markers = "sys_platform == \"win32\""}
+sse-starlette = ">=1.6.1"
+starlette = ">=0.27"
+typing-extensions = ">=4.9.0"
+typing-inspection = ">=0.4.1"
+uvicorn = {version = ">=0.31.1", markers = "sys_platform != \"emscripten\""}
+
+[package.extras]
+cli = ["python-dotenv (>=1.0.0)", "typer (>=0.16.0)"]
+rich = ["rich (>=13.9.4)"]
+ws = ["websockets (>=15.0.1)"]
+
[[package]]
name = "mdurl"
version = "0.1.2"
@@ -2557,7 +2681,7 @@ files = [
{file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"},
{file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"},
]
-markers = {main = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and implementation_name != \"PyPy\"", dev = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\""}
+markers = {main = "(platform_python_implementation == \"CPython\" and sys_platform == \"win32\" or platform_python_implementation != \"PyPy\") and implementation_name != \"PyPy\"", dev = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\""}
[[package]]
name = "pydantic"
@@ -2565,7 +2689,7 @@ version = "2.12.5"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.9"
-groups = ["dev"]
+groups = ["main", "dev"]
files = [
{file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"},
{file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"},
@@ -2587,7 +2711,7 @@ version = "2.41.5"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.9"
-groups = ["dev"]
+groups = ["main", "dev"]
files = [
{file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"},
{file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"},
@@ -2715,6 +2839,30 @@ files = [
[package.dependencies]
typing-extensions = ">=4.14.1"
+[[package]]
+name = "pydantic-settings"
+version = "2.13.1"
+description = "Settings management using Pydantic"
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237"},
+ {file = "pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025"},
+]
+
+[package.dependencies]
+pydantic = ">=2.7.0"
+python-dotenv = ">=0.21.0"
+typing-inspection = ">=0.4.0"
+
+[package.extras]
+aws-secrets-manager = ["boto3 (>=1.35.0)", "boto3-stubs[secretsmanager]"]
+azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"]
+gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"]
+toml = ["tomli (>=2.0.1)"]
+yaml = ["pyyaml (>=6.0.1)"]
+
[[package]]
name = "pydocstyle"
version = "6.3.0"
@@ -2760,6 +2908,28 @@ files = [
[package.extras]
windows-terminal = ["colorama (>=0.4.6)"]
+[[package]]
+name = "pyjwt"
+version = "2.12.1"
+description = "JSON Web Token implementation in Python"
+optional = false
+python-versions = ">=3.9"
+groups = ["main"]
+files = [
+ {file = "pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c"},
+ {file = "pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b"},
+]
+
+[package.dependencies]
+cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""}
+typing_extensions = {version = ">=4.0", markers = "python_version < \"3.11\""}
+
+[package.extras]
+crypto = ["cryptography (>=3.4.0)"]
+dev = ["coverage[toml] (==7.10.7)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=8.4.2,<9.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"]
+docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
+tests = ["coverage[toml] (==7.10.7)", "pytest (>=8.4.2,<9.0.0)"]
+
[[package]]
name = "pyomo"
version = "6.10.0"
@@ -2913,6 +3083,18 @@ files = [
[package.extras]
cli = ["click (>=5.0)"]
+[[package]]
+name = "python-multipart"
+version = "0.0.22"
+description = "A streaming multipart parser for Python"
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155"},
+ {file = "python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58"},
+]
+
[[package]]
name = "python-slugify"
version = "8.0.4"
@@ -2943,6 +3125,37 @@ files = [
{file = "pytz-2023.4.tar.gz", hash = "sha256:31d4583c4ed539cd037956140d695e42c033a19e984bfce9964a3f7d59bc2b40"},
]
+[[package]]
+name = "pywin32"
+version = "311"
+description = "Python for Window Extensions"
+optional = false
+python-versions = "*"
+groups = ["main"]
+markers = "sys_platform == \"win32\""
+files = [
+ {file = "pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3"},
+ {file = "pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b"},
+ {file = "pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b"},
+ {file = "pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151"},
+ {file = "pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503"},
+ {file = "pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2"},
+ {file = "pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31"},
+ {file = "pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067"},
+ {file = "pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852"},
+ {file = "pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d"},
+ {file = "pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d"},
+ {file = "pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a"},
+ {file = "pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee"},
+ {file = "pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87"},
+ {file = "pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42"},
+ {file = "pywin32-311-cp38-cp38-win32.whl", hash = "sha256:6c6f2969607b5023b0d9ce2541f8d2cbb01c4f46bc87456017cf63b73f1e2d8c"},
+ {file = "pywin32-311-cp38-cp38-win_amd64.whl", hash = "sha256:c8015b09fb9a5e188f83b7b04de91ddca4658cee2ae6f3bc483f0b21a77ef6cd"},
+ {file = "pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b"},
+ {file = "pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91"},
+ {file = "pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d"},
+]
+
[[package]]
name = "pyyaml"
version = "6.0.3"
@@ -3488,6 +3701,47 @@ flask-login = ["Flask-Login (>=0.2.9)"]
flask-sqlalchemy = ["Flask-SQLAlchemy (>=1.0)"]
test = ["Flask (>=0.9)", "Flask-Login (>=0.2.9)", "Flask-SQLAlchemy (>=1.0)", "PyMySQL (>=0.8.0)", "packaging", "psycopg2 (>=2.4.6)", "pytest (>=2.3.5)"]
+[[package]]
+name = "sse-starlette"
+version = "3.3.4"
+description = "SSE plugin for Starlette"
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "sse_starlette-3.3.4-py3-none-any.whl", hash = "sha256:84bb06e58939a8b38d8341f1bc9792f06c2b53f48c608dd207582b664fc8f3c1"},
+ {file = "sse_starlette-3.3.4.tar.gz", hash = "sha256:aaf92fc067af8a5427192895ac028e947b484ac01edbc3caf00e7e7137c7bef1"},
+]
+
+[package.dependencies]
+anyio = ">=4.7.0"
+starlette = ">=0.49.1"
+
+[package.extras]
+daphne = ["daphne (>=4.2.0)"]
+examples = ["aiosqlite (>=0.21.0)", "fastapi (>=0.115.12)", "sqlalchemy[asyncio] (>=2.0.41)", "uvicorn (>=0.34.0)"]
+granian = ["granian (>=2.3.1)"]
+uvicorn = ["uvicorn (>=0.34.0)"]
+
+[[package]]
+name = "starlette"
+version = "1.0.0"
+description = "The little ASGI library that shines."
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+files = [
+ {file = "starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b"},
+ {file = "starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149"},
+]
+
+[package.dependencies]
+anyio = ">=3.6.2,<5"
+typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""}
+
+[package.extras]
+full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"]
+
[[package]]
name = "stringcase"
version = "1.2.0"
@@ -3646,7 +3900,7 @@ version = "0.4.2"
description = "Runtime typing introspection tools"
optional = false
python-versions = ">=3.9"
-groups = ["dev"]
+groups = ["main", "dev"]
files = [
{file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"},
{file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"},
@@ -3713,6 +3967,27 @@ files = [
{file = "urlobject-3.0.0.tar.gz", hash = "sha256:bfdfe70746d92a039a33e964959bb12cecd9807a434fdb7fef5f38e70a295818"},
]
+[[package]]
+name = "uvicorn"
+version = "0.43.0"
+description = "The lightning-fast ASGI server."
+optional = false
+python-versions = ">=3.10"
+groups = ["main"]
+markers = "sys_platform != \"emscripten\""
+files = [
+ {file = "uvicorn-0.43.0-py3-none-any.whl", hash = "sha256:46fac64f487fd968cd999e5e49efbbe64bd231b5bd8b4a0b482a23ebce499620"},
+ {file = "uvicorn-0.43.0.tar.gz", hash = "sha256:ab1652d2fb23abf124f36ccc399828558880def222c3cb3d98d24021520dc6e8"},
+]
+
+[package.dependencies]
+click = ">=7.0"
+h11 = ">=0.8"
+typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""}
+
+[package.extras]
+standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.20)", "websockets (>=10.4)"]
+
[[package]]
name = "validators"
version = "0.35.0"
@@ -3953,4 +4228,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"]
[metadata]
lock-version = "2.1"
python-versions = ">=3.10,<4"
-content-hash = "6ce6e9842d753021fea22eb56323b703d9c673bd4e953cddf724b02c2f895029"
+content-hash = "5d4219e1ddb8f971d5f7e30c4e5aecccce1fd3572d76885758231059abfe0da4"
diff --git a/pyproject.toml b/pyproject.toml
index be98f16b..3ad13467 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -59,6 +59,7 @@ huggingface-hub = "^0.35.0"
pyomo = "^6.10.0"
highspy = "^1.13.1"
numpy = "^2.0.0"
+mcp = "^1.27.0"
[tool.poetry.group.dev.dependencies]
factory-boy = "*"