diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 2964e11..e3c9c00 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/mcp.cpython-311.pyc b/core/__pycache__/mcp.cpython-311.pyc new file mode 100644 index 0000000..2b38bd9 Binary files /dev/null and b/core/__pycache__/mcp.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 18a063c..0e37a4a 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index ebb8c6e..fdbc967 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 8d204fa..ed3679d 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 8c38f3f..4121599 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,17 @@ from django.contrib import admin +from .models import MCPToolRequest, HumanResponse, SlackSettings -# Register your models here. +@admin.register(SlackSettings) +class SlackSettingsAdmin(admin.ModelAdmin): + list_display = ('__str__', 'updated_at') + +@admin.register(MCPToolRequest) +class MCPToolRequestAdmin(admin.ModelAdmin): + list_display = ('id', 'tool_name', 'status', 'created_at') + list_filter = ('status', 'tool_name') + readonly_fields = ('id', 'created_at', 'updated_at') + +@admin.register(HumanResponse) +class HumanResponseAdmin(admin.ModelAdmin): + list_display = ('id', 'request', 'user_name', 'created_at') + readonly_fields = ('id', 'created_at') \ No newline at end of file diff --git a/core/mcp.py b/core/mcp.py new file mode 100644 index 0000000..90a48bc --- /dev/null +++ b/core/mcp.py @@ -0,0 +1,190 @@ +import json +import os +import httpx +from django.conf import settings +from .models import MCPToolRequest, HumanResponse, SlackSettings +from asgiref.sync import sync_to_async + +SLACK_API_URL = "https://slack.com/api/chat.postMessage" + +@sync_to_async +def get_slack_token(): + token = os.getenv("SLACK_BOT_TOKEN") + if token: + return token + config = SlackSettings.objects.first() + if config and config.bot_token: + return config.bot_token + return None + +async def send_slack_message(channel, text, thread_ts=None): + token = await get_slack_token() + if not token: + return {"error": "SLACK_BOT_TOKEN not configured", "ok": False} + + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json; charset=utf-8" + } + payload = { + "channel": channel, + "text": text, + } + if thread_ts: + payload["thread_ts"] = thread_ts + + async with httpx.AsyncClient() as client: + response = await client.post(SLACK_API_URL, headers=headers, json=payload) + data = response.json() + return data + +async def handle_mcp_request(body): + """ + Simple MCP JSON-RPC handler for HTTP/SSE. + Supports tools/list and tools/call. + """ + try: + request_data = json.loads(body) + except json.JSONDecodeError: + return {"jsonrpc": "2.0", "error": {"code": -32700, "message": "Parse error"}, "id": None} + + method = request_data.get("method") + params = request_data.get("params", {}) + request_id = request_data.get("id") + + if method == "initialize": + return { + "jsonrpc": "2.0", + "result": { + "protocolVersion": "2024-11-05", + "capabilities": { + "tools": {} + }, + "serverInfo": { + "name": "flatlogic-slack", + "version": "1.0.0" + } + }, + "id": request_id + } + + elif method == "notifications/initialized": + return None # No response for notifications + + elif method == "ping": + return { + "jsonrpc": "2.0", + "result": {}, + "id": request_id + } + + elif method == "tools/list": + return { + "jsonrpc": "2.0", + "result": { + "tools": [ + { + "name": "send_slack_message", + "description": "Sends a message to a Slack channel.", + "inputSchema": { + "type": "object", + "properties": { + "channel": {"type": "string", "description": "Channel ID or Name"}, + "text": {"type": "string", "description": "Message content"} + }, + "required": ["channel", "text"] + } + }, + { + "name": "list_responses", + "description": "Lists human responses for a specific request ID.", + "inputSchema": { + "type": "object", + "properties": { + "request_id": {"type": "string", "description": "The UUID of the original request"} + }, + "required": ["request_id"] + } + } + ] + }, + "id": request_id + } + + elif method == "tools/call": + tool_name = params.get("name") + arguments = params.get("arguments", {}) + + if tool_name == "send_slack_message": + channel = arguments.get("channel") + text = arguments.get("text") + + # Log request + db_request = await MCPToolRequest.objects.acreate( + tool_name=tool_name, + arguments=arguments, + slack_channel=channel or "" + ) + + slack_result = await send_slack_message(channel, text) + + if slack_result.get("ok"): + db_request.status = 'SENT' + db_request.slack_ts = slack_result.get("ts", "") + db_request.response_json = slack_result + await db_request.asave() + + return { + "jsonrpc": "2.0", + "result": { + "content": [ + { + "type": "text", + "text": f"Message sent to Slack (TS: {db_request.slack_ts}). Request ID: {db_request.id}" + } + ] + }, + "id": request_id + } + else: + db_request.status = 'ERROR' + db_request.response_json = slack_result + await db_request.asave() + return { + "jsonrpc": "2.0", + "error": { + "code": -32000, + "message": f"Slack API Error: {slack_result.get('error')}" + }, + "id": request_id + } + + elif tool_name == "list_responses": + req_id = arguments.get("request_id") + responses = [] + async for r in HumanResponse.objects.filter(request_id=req_id): + responses.append({ + "text": r.text, + "user": r.user_name, + "file_url": r.file_url, + "timestamp": str(r.created_at) + }) + + return { + "jsonrpc": "2.0", + "result": { + "content": [ + { + "type": "text", + "text": json.dumps(responses, indent=2) if responses else "No responses yet." + } + ] + }, + "id": request_id + } + + return { + "jsonrpc": "2.0", + "error": {"code": -32601, "message": "Method not found"}, + "id": request_id + } diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..b96f6fd --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,41 @@ +# Generated by Django 5.2.7 on 2026-02-25 23:18 + +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='MCPToolRequest', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('tool_name', models.CharField(max_length=100)), + ('arguments', models.JSONField()), + ('status', models.CharField(choices=[('PENDING', 'Pending'), ('SENT', 'Sent'), ('ERROR', 'Error')], default='PENDING', max_length=10)), + ('slack_channel', models.CharField(blank=True, max_length=50)), + ('slack_ts', models.CharField(blank=True, help_text='Slack message timestamp', max_length=50)), + ('response_json', models.JSONField(blank=True, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + ), + migrations.CreateModel( + name='HumanResponse', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('text', models.TextField()), + ('user_name', models.CharField(blank=True, max_length=100)), + ('file_url', models.URLField(blank=True, max_length=500, null=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('request', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='core.mcptoolrequest')), + ], + ), + ] diff --git a/core/migrations/0002_slacksettings.py b/core/migrations/0002_slacksettings.py new file mode 100644 index 0000000..68b0b42 --- /dev/null +++ b/core/migrations/0002_slacksettings.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.7 on 2026-02-25 23:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='SlackSettings', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('bot_token', models.CharField(blank=True, max_length=255, null=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ], + options={ + 'verbose_name_plural': 'Slack Settings', + }, + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..21958eb Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0002_slacksettings.cpython-311.pyc b/core/migrations/__pycache__/0002_slacksettings.cpython-311.pyc new file mode 100644 index 0000000..3f3645c Binary files /dev/null and b/core/migrations/__pycache__/0002_slacksettings.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..0bf39ed 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,43 @@ from django.db import models +import uuid -# Create your models here. +class SlackSettings(models.Model): + """Stores the Slack App Token so users don't have to edit .env files.""" + bot_token = models.CharField(max_length=255, blank=True, null=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return "Slack Settings" + + class Meta: + verbose_name_plural = "Slack Settings" + +class MCPToolRequest(models.Model): + STATUS_CHOICES = [ + ('PENDING', 'Pending'), + ('SENT', 'Sent'), + ('ERROR', 'Error'), + ] + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + tool_name = models.CharField(max_length=100) + arguments = models.JSONField() + status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='PENDING') + slack_channel = models.CharField(max_length=50, blank=True) + slack_ts = models.CharField(max_length=50, blank=True, help_text="Slack message timestamp") + response_json = models.JSONField(null=True, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __str__(self): + return f"{self.tool_name} - {self.status} ({self.created_at})" + +class HumanResponse(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + request = models.ForeignKey(MCPToolRequest, related_name='responses', on_delete=models.CASCADE) + text = models.TextField() + user_name = models.CharField(max_length=100, blank=True) + file_url = models.URLField(max_length=500, blank=True, null=True) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"Response to {self.request.id} - {self.created_at}" diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..2701929 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -2,24 +2,56 @@
- -