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') }} +
+
+ + + Connect to MCP + +

+ Connect your AI agent or MCP client to Dribdat. +

+
+
+
{% 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 = "*"