Autosave: 20260226-002417
This commit is contained in:
parent
79c28f68a7
commit
70e9a21b87
Binary file not shown.
BIN
core/__pycache__/mcp.cpython-311.pyc
Normal file
BIN
core/__pycache__/mcp.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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')
|
||||
190
core/mcp.py
Normal file
190
core/mcp.py
Normal file
@ -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
|
||||
}
|
||||
41
core/migrations/0001_initial.py
Normal file
41
core/migrations/0001_initial.py
Normal file
@ -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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
24
core/migrations/0002_slacksettings.py
Normal file
24
core/migrations/0002_slacksettings.py
Normal file
@ -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',
|
||||
},
|
||||
),
|
||||
]
|
||||
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0001_initial.cpython-311.pyc
Normal file
Binary file not shown.
BIN
core/migrations/__pycache__/0002_slacksettings.cpython-311.pyc
Normal file
BIN
core/migrations/__pycache__/0002_slacksettings.cpython-311.pyc
Normal file
Binary file not shown.
@ -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}"
|
||||
|
||||
@ -2,24 +2,56 @@
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{% block title %}Knowledge Base{% endblock %}</title>
|
||||
{% if project_description %}
|
||||
<meta name="description" content="{{ project_description }}">
|
||||
<meta property="og:description" content="{{ project_description }}">
|
||||
<meta property="twitter:description" content="{{ project_description }}">
|
||||
{% endif %}
|
||||
{% if project_image_url %}
|
||||
<meta property="og:image" content="{{ project_image_url }}">
|
||||
<meta property="twitter:image" content="{{ project_image_url }}">
|
||||
{% endif %}
|
||||
{% load static %}
|
||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
||||
{% block head %}{% endblock %}
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}MCP Slack Server{% endblock %}</title>
|
||||
{% if project_description %}
|
||||
<meta name="description" content="{{ project_description }}">
|
||||
{% endif %}
|
||||
{% load static %}
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- Google Fonts: Inter & JetBrains Mono -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{% block content %}{% endblock %}
|
||||
<nav class="navbar navbar-expand-lg sticky-top">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="{% url 'home' %}">
|
||||
<span class="status-dot online"></span>MCP SLACK SERVER
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'home' %}">Dashboard</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="/admin/">Admin</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="py-5 mt-5 border-top border-secondary">
|
||||
<div class="container text-center text-secondary">
|
||||
<p>© 2026 MCP Slack Bridge • Powered by Flatlogic AI</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Bootstrap 5 JS Bundle -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
@ -1,145 +1,144 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{{ project_name }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg-color-start: #6a11cb;
|
||||
--bg-color-end: #2575fc;
|
||||
--text-color: #ffffff;
|
||||
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
|
||||
animation: bg-pan 20s linear infinite;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
@keyframes bg-pan {
|
||||
0% {
|
||||
background-position: 0% 0%;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 100% 100%;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--card-bg-color);
|
||||
border: 1px solid var(--card-border-color);
|
||||
border-radius: 16px;
|
||||
padding: 2.5rem 2rem;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
|
||||
font-weight: 700;
|
||||
margin: 0 0 1.2rem;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.loader {
|
||||
margin: 1.5rem auto;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.25);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.runtime code {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
padding: 0.15rem 0.45rem;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
border: 0;
|
||||
}
|
||||
|
||||
footer {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
opacity: 0.75;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block title %}Dashboard | MCP Slack Bridge{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main>
|
||||
<div class="card">
|
||||
<h1>Analyzing your requirements and generating your app…</h1>
|
||||
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||
<span class="sr-only">Loading…</span>
|
||||
<section class="hero-section">
|
||||
<div class="container">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-lg-8">
|
||||
<h1 class="display-4 fw-bold mb-4">AI &Human Slack Bridge</h1>
|
||||
<p class="lead text-secondary mb-5">
|
||||
Bridging the gap between autonomous AI agents and human workers.
|
||||
Exposing tools to request design, feedback, or any manual task directly via Slack.
|
||||
</p>
|
||||
<div class="d-flex gap-3">
|
||||
<span class="badge-online d-flex align-items-center">
|
||||
<span class="status-dot online"></span>
|
||||
Server: {{ server_status }}
|
||||
</span>
|
||||
<span class="badge rounded-pill border border-secondary text-secondary d-flex align-items-center px-3">
|
||||
DJANGO 5.2 • MCP v1.0
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-4 d-none d-lg-block">
|
||||
<div class="card p-4 text-center shadow">
|
||||
<h5 class="mb-3 text-secondary">MCP Endpoint</h5>
|
||||
<code class="d-block py-2">/mcp/</code>
|
||||
<p class="small text-secondary mt-3 mb-0">Post JSON-RPC here</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p>
|
||||
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
|
||||
<p class="runtime">
|
||||
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
|
||||
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
|
||||
</footer>
|
||||
{% endblock %}
|
||||
</section>
|
||||
|
||||
<div class="container my-5">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% if not is_slack_configured %}
|
||||
<div class="alert alert-warning border-warning bg-opacity-10 bg-warning mb-5">
|
||||
<h5 class="alert-heading fw-bold">Configuration Required</h5>
|
||||
<p class="mb-0">
|
||||
Set <code>SLACK_BOT_TOKEN</code> in your environment to enable message delivery.
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card mb-5">
|
||||
<div class="card-body p-0">
|
||||
<div class="p-4 border-bottom border-secondary d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0 text-white">Recent Tool Activity</h5>
|
||||
<small class="text-secondary">Showing last 20 requests</small>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-borderless table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="ps-4">TIMESTAMP</th>
|
||||
<th>TOOL NAME</th>
|
||||
<th>CHANNEL</th>
|
||||
<th>STATUS</th>
|
||||
<th class="pe-4 text-end">ACTION</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for r in requests %}
|
||||
<tr class="log-entry" onclick="location.href='{% url 'request_detail' r.pk %}'">
|
||||
<td class="ps-4 text-secondary small">{{ r.created_at|date:"Y-m-d H:i" }}</td>
|
||||
<td><code class="text-info">{{ r.tool_name }}</code></td>
|
||||
<td class="text-secondary">#{{ r.slack_channel }}</td>
|
||||
<td>
|
||||
{% if r.status == 'SENT' %}
|
||||
<span class="badge-online bg-opacity-25 bg-success text-success border border-success">SENT</span>
|
||||
{% elif r.status == 'ERROR' %}
|
||||
<span class="badge-error bg-opacity-25 bg-danger text-danger border border-danger">ERROR</span>
|
||||
{% else %}
|
||||
<span class="badge-pending bg-opacity-25 bg-warning text-warning border border-warning">PENDING</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="pe-4 text-end">
|
||||
<a href="{% url 'request_detail' r.pk %}" class="btn btn-sm btn-outline-secondary">Details</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center py-5 text-secondary italic">
|
||||
No activity logged yet. Connect an MCP client to start.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100 p-4">
|
||||
<h5 class="text-white mb-4 fw-bold">How to use (Tools/Call)</h5>
|
||||
<pre class="small mb-0">
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "tools/call",
|
||||
"params": {
|
||||
"name": "send_slack_message",
|
||||
"arguments": {
|
||||
"channel": "C12345",
|
||||
"text": "Hello Human!"
|
||||
}
|
||||
},
|
||||
"id": 1
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 mt-4 mt-lg-0">
|
||||
<div class="card h-100 p-4">
|
||||
<h5 class="text-white mb-4 fw-bold">MCP Server Info</h5>
|
||||
<ul class="list-unstyled text-secondary">
|
||||
<li class="mb-3 d-flex justify-content-between">
|
||||
<span>Status:</span>
|
||||
<span class="text-success fw-bold">ONLINE</span>
|
||||
</li>
|
||||
<li class="mb-3 d-flex justify-content-between">
|
||||
<span>Version:</span>
|
||||
<span>1.0.0</span>
|
||||
</li>
|
||||
<li class="mb-3 d-flex justify-content-between">
|
||||
<span>Available Tools:</span>
|
||||
<span>2</span>
|
||||
</li>
|
||||
<li class="mb-0 d-flex justify-content-between">
|
||||
<span>Python:</span>
|
||||
<span>{{ python_version|default:"3.11" }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
98
core/templates/core/request_detail.html
Normal file
98
core/templates/core/request_detail.html
Normal file
@ -0,0 +1,98 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Request Details | MCP Slack Bridge{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container my-5 py-5">
|
||||
<div class="row">
|
||||
<div class="col-lg-10 mx-auto">
|
||||
<nav aria-label="breadcrumb" class="mb-4">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'home' %}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item active text-secondary" aria-current="page">Request: {{ req.id|truncatechars:12 }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="card p-4 p-md-5">
|
||||
<div class="d-flex justify-content-between align-items-center mb-5">
|
||||
<h2 class="text-white fw-bold mb-0">Tool Call Details</h2>
|
||||
{% if req.status == 'SENT' %}
|
||||
<span class="badge-online bg-opacity-25 bg-success text-success border border-success">SENT</span>
|
||||
{% elif req.status == 'ERROR' %}
|
||||
<span class="badge-error bg-opacity-25 bg-danger text-danger border border-danger">ERROR</span>
|
||||
{% else %}
|
||||
<span class="badge-pending bg-opacity-25 bg-warning text-warning border border-warning">PENDING</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<p class="small text-secondary fw-bold text-uppercase">Tool Name</p>
|
||||
<h5 class="text-info">{{ req.tool_name }}</h5>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<p class="small text-secondary fw-bold text-uppercase">Slack Channel</p>
|
||||
<h5 class="text-white">#{{ req.slack_channel|default:"N/A" }}</h5>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<p class="small text-secondary fw-bold text-uppercase">Slack Timestamp</p>
|
||||
<h5 class="text-white">{{ req.slack_ts|default:"N/A" }}</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="my-5 border-secondary opacity-25">
|
||||
|
||||
<div class="mb-5">
|
||||
<h5 class="text-white mb-4">Arguments</h5>
|
||||
<pre class="small bg-dark p-4 rounded-3 text-info"><code>{{ req.arguments|json_script:"args-data" }}</code>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const data = JSON.parse(document.getElementById('args-data').textContent);
|
||||
document.querySelector('pre code').textContent = JSON.stringify(data, null, 2);
|
||||
});
|
||||
</script></pre>
|
||||
</div>
|
||||
|
||||
<div class="mb-5">
|
||||
<h5 class="text-white mb-4">Slack API Response</h5>
|
||||
<pre class="small bg-dark p-4 rounded-3 text-secondary"><code>{{ req.response_json|json_script:"resp-data" }}</code>
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const data = JSON.parse(document.getElementById('resp-data').textContent);
|
||||
document.querySelectorAll('pre code')[1].textContent = JSON.stringify(data, null, 2);
|
||||
});
|
||||
</script></pre>
|
||||
</div>
|
||||
|
||||
<div class="responses-section">
|
||||
<h5 class="text-white mb-4 d-flex align-items-center">
|
||||
Human Responses
|
||||
<span class="ms-3 badge rounded-pill bg-primary fs-6">{{ responses.count }}</span>
|
||||
</h5>
|
||||
|
||||
{% for r in responses %}
|
||||
<div class="card bg-opacity-10 bg-info border-info border-opacity-25 p-4 mb-3">
|
||||
<div class="d-flex justify-content-between mb-3">
|
||||
<span class="fw-bold text-info">{{ r.user_name|default:"Human" }}</span>
|
||||
<span class="small text-secondary">{{ r.created_at|date:"Y-m-d H:i" }}</span>
|
||||
</div>
|
||||
<p class="mb-0 text-white">{{ r.text }}</p>
|
||||
{% if r.file_url %}
|
||||
<div class="mt-3">
|
||||
<a href="{{ r.file_url }}" target="_blank" class="btn btn-sm btn-outline-info">View Attachment</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="text-center py-5 border border-dashed border-secondary rounded-3">
|
||||
<p class="text-secondary italic mb-0">No responses recorded yet.</p>
|
||||
<small class="text-secondary">AI agent can check for updates using <code>list_responses</code></small>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
10
core/urls.py
10
core/urls.py
@ -1,7 +1,9 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import home
|
||||
from .views import dashboard, mcp_endpoint, slack_webhook, request_detail
|
||||
|
||||
urlpatterns = [
|
||||
path("", home, name="home"),
|
||||
]
|
||||
path("", dashboard, name="home"),
|
||||
path("mcp/", mcp_endpoint, name="mcp_endpoint"),
|
||||
path("webhook/slack/", slack_webhook, name="slack_webhook"),
|
||||
path("request/<uuid:pk>/", request_detail, name="request_detail"),
|
||||
]
|
||||
119
core/views.py
119
core/views.py
@ -1,25 +1,112 @@
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
|
||||
from django import get_version as django_version
|
||||
from django.shortcuts import render
|
||||
from django.http import JsonResponse, HttpResponse
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from asgiref.sync import sync_to_async
|
||||
from .models import MCPToolRequest, HumanResponse, SlackSettings
|
||||
from .mcp import handle_mcp_request
|
||||
|
||||
@csrf_exempt
|
||||
async def mcp_endpoint(request):
|
||||
"""
|
||||
Main entry point for MCP clients via HTTP.
|
||||
Accepts JSON-RPC 2.0 payloads.
|
||||
"""
|
||||
if request.method != "POST":
|
||||
return JsonResponse({"error": "Method not allowed"}, status=405)
|
||||
|
||||
body = request.body.decode("utf-8")
|
||||
response_data = await handle_mcp_request(body)
|
||||
|
||||
if response_data is None:
|
||||
return HttpResponse(status=204)
|
||||
|
||||
return JsonResponse(response_data)
|
||||
|
||||
def home(request):
|
||||
"""Render the landing screen with loader and environment details."""
|
||||
host_name = request.get_host().lower()
|
||||
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
|
||||
now = timezone.now()
|
||||
def dashboard(request):
|
||||
"""
|
||||
Polished landing page showing active server status and recent logs.
|
||||
"""
|
||||
requests_list = MCPToolRequest.objects.all().order_by('-created_at')[:20]
|
||||
|
||||
# Slack config check
|
||||
slack_token = os.getenv("SLACK_BOT_TOKEN")
|
||||
if not slack_token:
|
||||
config = SlackSettings.objects.first()
|
||||
if config and config.bot_token:
|
||||
slack_token = config.bot_token
|
||||
|
||||
is_slack_configured = bool(slack_token)
|
||||
|
||||
context = {
|
||||
"project_name": "New Style",
|
||||
"agent_brand": agent_brand,
|
||||
"django_version": django_version(),
|
||||
"python_version": platform.python_version(),
|
||||
"current_time": now,
|
||||
"host_name": host_name,
|
||||
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
||||
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
||||
"is_slack_configured": is_slack_configured,
|
||||
"requests": requests_list,
|
||||
"django_version": platform.python_version(),
|
||||
"now": timezone.now(),
|
||||
"server_status": "ONLINE" if is_slack_configured else "CONFIG_PENDING"
|
||||
}
|
||||
return render(request, "core/index.html", context)
|
||||
|
||||
@csrf_exempt
|
||||
def slack_webhook(request):
|
||||
"""
|
||||
Slack Event API Webhook.
|
||||
Captures threaded replies and saves them as HumanResponse.
|
||||
"""
|
||||
if request.method == "POST":
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
|
||||
# 1. Verification Challenge
|
||||
if data.get("type") == "url_verification":
|
||||
return JsonResponse({"challenge": data.get("challenge")})
|
||||
|
||||
# 2. Event Callback
|
||||
if data.get("type") == "event_callback":
|
||||
event = data.get("event", {})
|
||||
|
||||
# We only care about user messages (no bot_id) in threads (has thread_ts)
|
||||
if event.get("type") == "message" and not event.get("bot_id"):
|
||||
thread_ts = event.get("thread_ts")
|
||||
text = event.get("text", "")
|
||||
user_id = event.get("user", "Unknown User")
|
||||
|
||||
# Basic extraction
|
||||
user_name = user_id
|
||||
|
||||
files = event.get("files", [])
|
||||
file_url = None
|
||||
if files:
|
||||
file_url = files[0].get("url_private", "")
|
||||
|
||||
if thread_ts:
|
||||
# Find the corresponding tool request
|
||||
mcp_request = MCPToolRequest.objects.filter(slack_ts=thread_ts).first()
|
||||
if mcp_request:
|
||||
# Avoid duplicate responses if Slack retries
|
||||
# We can just check if we already have this text/user in last 10 seconds, or just save it
|
||||
HumanResponse.objects.create(
|
||||
request=mcp_request,
|
||||
text=text,
|
||||
user_name=user_name,
|
||||
file_url=file_url
|
||||
)
|
||||
return HttpResponse("Created", status=201)
|
||||
except Exception as e:
|
||||
print("Webhook error:", e)
|
||||
return HttpResponse("Error", status=400)
|
||||
|
||||
return HttpResponse("OK")
|
||||
|
||||
def request_detail(request, pk):
|
||||
"""Detailed view of a single tool call."""
|
||||
tool_request = get_object_or_404(MCPToolRequest, pk=pk)
|
||||
responses = tool_request.responses.all().order_by('-created_at')
|
||||
|
||||
return render(request, "core/request_detail.html", {
|
||||
"req": tool_request,
|
||||
"responses": responses
|
||||
})
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
Django==5.2.7
|
||||
mysqlclient==2.2.7
|
||||
python-dotenv==1.1.1
|
||||
httpx==0.27.0
|
||||
@ -1,4 +1,156 @@
|
||||
/* Custom styles for the application */
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
:root {
|
||||
--bg-color: #0d1117;
|
||||
--card-bg: #161b22;
|
||||
--text-primary: #c9d1d9;
|
||||
--text-secondary: #8b949e;
|
||||
--accent-blue: #58a6ff;
|
||||
--accent-green: #238636;
|
||||
--border-color: #30363d;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-primary);
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background-color: var(--card-bg) !important;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.5px;
|
||||
color: var(--accent-blue) !important;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
color: var(--text-secondary) !important;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
color: var(--accent-blue);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--accent-blue);
|
||||
border-color: var(--accent-blue);
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #388bfd;
|
||||
border-color: #388bfd;
|
||||
}
|
||||
|
||||
.badge-online {
|
||||
background-color: var(--accent-green);
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
box-shadow: 0 0 10px rgba(35, 134, 54, 0.4);
|
||||
}
|
||||
|
||||
.badge-pending {
|
||||
background-color: #d29922;
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.badge-error {
|
||||
background-color: #f85149;
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.table {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.table thead th {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.table td {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.log-entry:hover {
|
||||
background-color: rgba(88, 166, 255, 0.05);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
padding: 80px 0;
|
||||
background: radial-gradient(circle at top right, rgba(88, 166, 255, 0.1), transparent);
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.status-dot.online {
|
||||
background-color: var(--accent-green);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(35, 134, 54, 0.7);
|
||||
}
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px rgba(35, 134, 54, 0);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(35, 134, 54, 0);
|
||||
}
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #0d1117;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
color: #ff7b72;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: #0d1117;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-primary);
|
||||
overflow-x: auto;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user