<!DOCTYPE html>
<html>
<head>
<title>Metabase MCP Configuration</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/github-markdown-css@5.1.0/github-markdown.min.css">
<script src="https://cdn.jsdelivr.net/npm/marked@4.0.0/marked.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="text"] {
width: 100%;
padding: 8px;
box-sizing: border-box;
}
button {
background-color: #4CAF50;
color: white;
padding: 10px 15px;
border: none;
cursor: pointer;
margin-right: 10px;
}
button:hover {
background-color: #45a049;
}
.test-button {
background-color: #2196F3;
}
.test-button:hover {
background-color: #0b7dda;
}
.message {
margin: 15px 0;
padding: 10px;
border-radius: 5px;
}
.success {
background-color: #d4edda;
color: #155724;
}
.error {
background-color: #f8d7da;
color: #721c24;
}
.result-area {
margin: 20px 0;
padding:.875rem;
background: #f5f5f5;
border-radius: 4px;
border: 1px solid #ddd;
min-height: 100px;
max-height: 400px;
overflow-y: auto;
white-space: pre-wrap;
display: none;
}
.section {
margin-top: 30px;
border-top: 1px solid #ddd;
padding-top: 20px;
}
textarea.form-control {
width: 100%;
padding: 8px;
box-sizing: border-box;
font-family: monospace;
border: 1px solid #ccc;
border-radius: 4px;
}
.markdown-body {
box-sizing: border-box;
min-width: 200px;
max-width: 100%;
padding: 15px;
background-color: #fff;
border-radius: 4px;
border: 1px solid #ddd;
color: #24292e;
font-weight: normal;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4 {
color: #24292e;
font-weight: 600;
margin-top: 24px;
margin-bottom: 16px;
}
.markdown-body table {
display: table;
width: 100%;
border-collapse: collapse;
margin-bottom: 16px;
border: 1px solid #ddd;
}
.markdown-body table th {
font-weight: 600;
padding: 8px 13px;
border: 1px solid #ddd;
background-color: #f6f8fa;
color: #24292e;
}
.markdown-body table td {
padding: 8px 13px;
border: 1px solid #ddd;
color: #24292e;
}
.markdown-body table tr {
background-color: #fff;
border-top: 1px solid #c6cbd1;
}
.markdown-body table tr:nth-child(2n) {
background-color: #f6f8fa;
}
.markdown-body p,
.markdown-body ul,
.markdown-body ol,
.markdown-body blockquote {
color: #24292e;
margin-bottom: 16px;
}
.markdown-body pre,
.markdown-body code {
background-color: #f6f8fa;
border-radius: 3px;
padding: 0.2em 0.4em;
color: #24292e;
font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
}
.result-container {
margin-top: 15px;
}
</style>
</head>
<body>
<h1>Metabase MCP Configuration</h1>
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="message {% if 'error' in messages[0].lower() %}error{% else %}success{% endif %}">
{{ messages[0] }}
</div>
{% endif %}
{% endwith %}
<form method="post" action="/save_config" id="config-form">
<div class="form-group">
<label for="metabase_url">Metabase URL:</label>
<input type="text" id="metabase_url" name="metabase_url" value="{{ metabase_url }}" placeholder="http://localhost:3000">
</div>
<div class="form-group">
<label for="api_key">API Key:</label>
<input type="password" id="api_key" name="api_key" class="form-control"
value="{{ api_key }}"
placeholder="{{ 'API key is set' if api_key_set else 'Enter your Metabase API key' }}"
required>
<small class="form-text text-muted">
{% if api_key_set %}
Your API key is stored encrypted. Enter a new key to change it.
{% else %}
Your API key will be stored encrypted and never displayed in plain text.
{% endif %}
</small>
</div>
<div class="form-group">
<button type="submit">Save Configuration</button>
<button type="button" class="test-button" id="test-connection">Test Connection</button>
</div>
</form>
<div id="connection-result" class="result-area"></div>
<div class="section">
<h2>Test Tools</h2>
<h3>List Databases</h3>
<button type="button" id="test-list-databases">Test List Databases</button>
<div id="list-databases-result" class="result-area"></div>
<h3>Get Database Metadata</h3>
<div class="form-group">
<label for="database_id">Database ID:</label>
<input type="text" id="database_id" placeholder="Enter database ID">
</div>
<button type="button" id="test-get-metadata">Test Get Metadata</button>
<div id="get-metadata-result" class="result-area"></div>
<h3>DB Overview</h3>
<div class="form-group">
<label for="db_overview_database_id">Database ID:</label>
<input type="text" id="db_overview_database_id" placeholder="Enter database ID">
</div>
<button type="button" id="test-db-overview">Get Database Overview</button>
<div id="db-overview-result" class="result-area"></div>
<h3>Table Detail</h3>
<div class="form-group">
<label for="table_detail_database_id">Database ID:</label>
<input type="text" id="table_detail_database_id" placeholder="Enter database ID">
</div>
<div class="form-group">
<label for="table_detail_table_id">Table ID:</label>
<input type="text" id="table_detail_table_id" placeholder="Enter table ID (from DB Overview)">
</div>
<button type="button" id="test-table-detail">Get Table Detail</button>
<div id="table-detail-result" class="result-area"></div>
<h3>Visualize Database Relationships</h3>
<div class="form-group">
<label for="relationship_database_id">Database ID:</label>
<input type="text" id="relationship_database_id" placeholder="Enter database ID">
</div>
<button type="button" id="test-visualize-relationships">Visualize Relationships</button>
<div id="visualize-relationships-result" class="result-area"></div>
<h3>Run Database Query</h3>
<div class="form-group">
<label for="query_database_id">Database ID:</label>
<input type="text" id="query_database_id" placeholder="Enter database ID">
</div>
<div class="form-group">
<label for="sql_query">SQL Query:</label>
<textarea id="sql_query" placeholder="Enter SQL query" rows="4" class="form-control"></textarea>
<small class="form-text text-muted">Query will be limited to returning 5 rows for safety.</small>
</div>
<button type="button" id="test-run-query">Run Query</button>
<div id="run-query-result" class="result-area"></div>
</div>
<div class="section">
<h3>List Actions</h3>
<button type="button" id="test-list-actions">Test List Actions</button>
<div id="list-actions-result" class="result-area"></div>
<h3>Get Action Details</h3>
<div class="form-group">
<label for="action_id">Action ID:</label>
<input type="text" id="action_id" placeholder="Enter action ID">
</div>
<button type="button" id="test-get-action">Test Get Action Details</button>
<div id="get-action-result" class="result-area"></div>
<h3>Execute Action</h3>
<div class="form-group">
<label for="exec_action_id">Action ID:</label>
<input type="text" id="exec_action_id" placeholder="Enter action ID">
</div>
<button type="button" id="load-action-params">Load Parameters</button>
<div id="action-parameters" class="form-group"></div>
<button type="button" id="test-execute-action">Execute Action</button>
<div id="execute-action-result" class="result-area"></div>
</div>
<script>
// Configure marked.js options
marked.setOptions({
gfm: true,
breaks: true,
tables: true,
sanitize: false
});
// Function to format response data with proper Markdown rendering
function formatResponse(container, data) {
if (data.success) {
// Check if response has result or message property
const content = data.result || data.message || '';
// Only try to parse as markdown if it's not empty
if (content) {
container.innerHTML = '<div class="markdown-body">' + marked.parse(content) + '</div>';
} else {
container.innerHTML = '<div class="markdown-body">Operation completed successfully.</div>';
}
// Add success class if not already present
if (!container.className.includes('success')) {
container.className += ' success';
}
} else {
const errorMsg = data.error || data.message || 'Unknown error';
container.innerHTML = '<div class="error">' + errorMsg + '</div>';
// Add error class if not already present
if (!container.className.includes('error')) {
container.className += ' error';
}
}
}
// Test connection
document.getElementById('test-connection').addEventListener('click', function() {
const url = document.getElementById('metabase_url').value;
const apiKey = document.getElementById('api_key').value;
const resultArea = document.getElementById('connection-result');
resultArea.style.display = 'block';
resultArea.innerHTML = 'Testing connection...';
fetch('/test_connection', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: `metabase_url=${encodeURIComponent(url)}&api_key=${encodeURIComponent(apiKey)}`
})
.then(response => response.json())
.then(data => {
formatResponse(resultArea, data);
})
.catch(error => {
resultArea.className = 'result-area error';
resultArea.innerHTML = `Error: ${error}`;
});
});
// Test list databases
document.getElementById('test-list-databases').addEventListener('click', function() {
const resultArea = document.getElementById('list-databases-result');
resultArea.style.display = 'block';
resultArea.innerHTML = 'Fetching databases...';
fetch('/test_list_databases')
.then(response => response.json())
.then(data => {
formatResponse(resultArea, data);
})
.catch(error => {
resultArea.className = 'result-area error';
resultArea.innerHTML = `Error: ${error}`;
});
});
// Test get metadata
document.getElementById('test-get-metadata').addEventListener('click', function() {
const databaseId = document.getElementById('database_id').value;
const resultArea = document.getElementById('get-metadata-result');
if (!databaseId) {
resultArea.style.display = 'block';
resultArea.className = 'result-area error';
resultArea.innerHTML = 'Please enter a database ID';
return;
}
resultArea.style.display = 'block';
resultArea.innerHTML = 'Fetching metadata...';
fetch('/test_get_metadata', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: `database_id=${encodeURIComponent(databaseId)}`
})
.then(response => response.json())
.then(data => {
formatResponse(resultArea, data);
})
.catch(error => {
resultArea.className = 'result-area error';
resultArea.innerHTML = `Error: ${error}`;
});
});
// Test list actions
document.getElementById('test-list-actions').addEventListener('click', function() {
const resultArea = document.getElementById('list-actions-result');
resultArea.style.display = 'block';
resultArea.innerHTML = 'Fetching actions...';
fetch('/test_list_actions')
.then(response => response.json())
.then(data => {
formatResponse(resultArea, data);
})
.catch(error => {
resultArea.className = 'result-area error';
resultArea.innerHTML = `Error: ${error}`;
});
});
// Test get action details
document.getElementById('test-get-action').addEventListener('click', function() {
const actionId = document.getElementById('action_id').value;
const resultArea = document.getElementById('get-action-result');
if (!actionId) {
resultArea.style.display = 'block';
resultArea.className = 'result-area error';
resultArea.innerHTML = 'Please enter an action ID';
return;
}
resultArea.style.display = 'block';
resultArea.innerHTML = 'Fetching action details...';
fetch('/test_get_action_details', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: `action_id=${encodeURIComponent(actionId)}`
})
.then(response => response.json())
.then(data => {
formatResponse(resultArea, data);
})
.catch(error => {
resultArea.className = 'result-area error';
resultArea.innerHTML = `Error: ${error}`;
});
});
// Load action parameters
document.getElementById('load-action-params').addEventListener('click', function() {
const actionId = document.getElementById('exec_action_id').value;
const paramsContainer = document.getElementById('action-parameters');
if (!actionId) {
alert('Please enter an action ID');
return;
}
paramsContainer.innerHTML = 'Loading parameters...';
fetch('/test_get_action_details', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: `action_id=${encodeURIComponent(actionId)}`
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Parse the markdown to extract parameters
const paramSection = data.result.split('### Parameters')[1];
if (!paramSection) {
paramsContainer.innerHTML = 'No parameters found for this action.';
return;
}
// Clear container
paramsContainer.innerHTML = '';
// Extract parameter names from the markdown
const paramLines = paramSection.split('\n');
let paramCount = 0;
for (const line of paramLines) {
if (line.startsWith('- **')) {
const paramName = line.split('**:')[0].replace('- **', '').trim();
const formGroup = document.createElement('div');
formGroup.className = 'form-group';
const label = document.createElement('label');
label.setAttribute('for', `param_${paramName}`);
label.textContent = `Parameter: ${paramName}`;
const input = document.createElement('input');
input.type = 'text';
input.id = `param_${paramName}`;
input.name = `param_${paramName}`;
input.placeholder = `Enter value for ${paramName}`;
formGroup.appendChild(label);
formGroup.appendChild(input);
paramsContainer.appendChild(formGroup);
paramCount++;
}
}
if (paramCount === 0) {
paramsContainer.innerHTML = 'No parameters found for this action.';
}
} else {
paramsContainer.innerHTML = `Error: ${data.error}`;
}
})
.catch(error => {
paramsContainer.innerHTML = `Error: ${error}`;
});
});
// Execute action
document.getElementById('test-execute-action').addEventListener('click', function() {
const actionId = document.getElementById('exec_action_id').value;
const resultArea = document.getElementById('execute-action-result');
const paramsContainer = document.getElementById('action-parameters');
if (!actionId) {
resultArea.style.display = 'block';
resultArea.className = 'result-area error';
resultArea.innerHTML = 'Please enter an action ID';
return;
}
// Collect parameters
const formData = new FormData();
formData.append('action_id', actionId);
// Add all input fields from the parameters container
const inputs = paramsContainer.querySelectorAll('input');
inputs.forEach(input => {
if (input.name && input.value) {
formData.append(input.name, input.value);
}
});
resultArea.style.display = 'block';
resultArea.innerHTML = 'Executing action...';
fetch('/test_execute_action', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
formatResponse(resultArea, data);
})
.catch(error => {
resultArea.className = 'result-area error';
resultArea.innerHTML = `Error: ${error}`;
});
});
// Test visualize relationships
document.getElementById('test-visualize-relationships').addEventListener('click', function() {
const databaseId = document.getElementById('relationship_database_id').value;
if (!databaseId) {
alert('Please enter a database ID');
return;
}
const resultArea = document.getElementById('visualize-relationships-result');
resultArea.style.display = 'block';
resultArea.innerHTML = 'Visualizing relationships...';
fetch('/test_visualize_relationships', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `database_id=${databaseId}`
})
.then(response => response.json())
.then(data => {
formatResponse(resultArea, data);
})
.catch(error => {
resultArea.innerHTML = `<div class="error">Error: ${error.message}</div>`;
});
});
// Test run query
document.getElementById('test-run-query').addEventListener('click', function() {
const databaseId = document.getElementById('query_database_id').value;
const sqlQuery = document.getElementById('sql_query').value;
if (!databaseId || !sqlQuery) {
alert('Please enter both database ID and SQL query');
return;
}
const resultArea = document.getElementById('run-query-result');
resultArea.style.display = 'block';
resultArea.innerHTML = 'Executing query...';
fetch('/test_run_query', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `database_id=${encodeURIComponent(databaseId)}&query=${encodeURIComponent(sqlQuery)}`
})
.then(response => response.json())
.then(data => {
formatResponse(resultArea, data);
})
.catch(error => {
resultArea.innerHTML = `<div class="error">Error: ${error.message}</div>`;
});
});
// Test DB Overview
document.getElementById('test-db-overview').addEventListener('click', function() {
const databaseId = document.getElementById('db_overview_database_id').value;
if (!databaseId) {
alert('Please enter a database ID');
return;
}
const resultArea = document.getElementById('db-overview-result');
resultArea.style.display = 'block';
resultArea.innerHTML = 'Loading database overview...';
fetch('/test_db_overview', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `database_id=${databaseId}`
})
.then(response => response.json())
.then(data => {
formatResponse(resultArea, data);
})
.catch(error => {
resultArea.innerHTML = `<div class="error">Error: ${error.message}</div>`;
});
});
// Test Table Detail
document.getElementById('test-table-detail').addEventListener('click', function() {
const databaseId = document.getElementById('table_detail_database_id').value;
const tableId = document.getElementById('table_detail_table_id').value;
if (!databaseId || !tableId) {
alert('Please enter both database ID and table ID');
return;
}
const resultArea = document.getElementById('table-detail-result');
resultArea.style.display = 'block';
resultArea.innerHTML = 'Loading table details...';
fetch('/test_table_detail', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: `database_id=${encodeURIComponent(databaseId)}&table_id=${encodeURIComponent(tableId)}`
})
.then(response => response.json())
.then(data => {
formatResponse(resultArea, data);
})
.catch(error => {
resultArea.innerHTML = `<div class="error">Error: ${error.message}</div>`;
});
});
</script>
</body>
</html>