micro--go-micro
96280678ee
* feat: expose framework primitives via API gateway and MCP
Add registry, store, and broker as both HTTP routes and MCP tools
so AI agents and HTTP clients can inspect and operate the framework.
API gateway (/micro/* namespace):
GET /micro/registry List registered services
GET /micro/registry/{name} Describe a service
GET /micro/store List store keys
GET /micro/store/{key} Read a record
POST /micro/store/{key} Write a record
POST /micro/broker/{topic} Publish a message
MCP gateway (micro_* tool prefix):
micro_registry_list List services
micro_registry_get Describe a service
micro_store_list List keys
micro_store_read Read a record
micro_store_write Write a record
micro_broker_publish Publish a message
Framework tools use a Handler field on the MCP Tool struct for
direct dispatch (no RPC). Service tools continue to use RPC.
Rate limiters and circuit breakers are applied to framework
tools the same as service tools.
* fix: make framework internals opt-in on API and MCP gateways
Framework primitives (registry, broker, store) are now only
exposed when explicitly enabled:
API gateway: micro api --internal
MCP gateway: Options{Internal: true}
Off by default — user services are always exposed, framework
internals require the flag. Banner output only shows framework
routes when enabled.
* fix: always expose framework internals, gate by auth in production
Revert the --internal flag approach. Framework primitives (registry,
broker, store) are now always exposed:
- micro api: /micro/* routes always available (dev tool)
- MCP gateway: micro_* tools always registered. When Auth is
configured (production), they require micro:admin scope.
Without Auth (dev), they're open — same as all other tools.
This follows the existing pattern: micro run/api = dev (open),
micro server = production (auth + scopes). Framework internals
follow the same security model as user services.
Remove the Internal option from MCP Options. Remove --internal
flag from micro api.
Note: scope persistence depends on the store backend. The default
in-memory store does not survive restarts. Use MICRO_STORE=file
for persistent scopes in production.
* fix: correct DefaultStore comment — it's file-backed, not memory
* fix(server): don't recreate deleted admin user on restart
When the default admin account is deleted via the dashboard, set
a marker key (auth/.admin-deleted) in the store. On startup, skip
admin creation if the marker exists. This prevents the default
admin/micro credentials from reappearing after restart when the
user has intentionally removed them.
* fix: improve agent playground first-run UX and fix doc 404s
Agent playground:
- Add setup hint in empty state explaining how to get started
(click Settings, enter API key, type a prompt)
- Hide hint automatically when API key is already configured
- Add all 7 providers to dropdown (was only OpenAI + Anthropic)
- Include CLI fallback suggestion (micro chat)
Docs:
- Fix .md links to .html across all doc pages — Jekyll serves
.html files, not .md. Fixes 404s including the micro run guide.
---------
Co-authored-by: Claude <noreply@anthropic.com>
356 行
13 KiB
HTML
356 行
13 KiB
HTML
{{define "content"}}
|
|
<style>
|
|
#agent-chat {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: calc(100vh - 160px);
|
|
max-height: 800px;
|
|
}
|
|
#agent-messages {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 1em 0;
|
|
scroll-behavior: smooth;
|
|
}
|
|
.msg { padding: 0.7em 1em; border-radius: 8px; margin-bottom: 0.6em; line-height: 1.6; }
|
|
.msg-user { background: #f0f4ff; border: 1px solid #d0d8f0; }
|
|
.msg-user b { color: #336; }
|
|
.msg-assistant { background: #fff; border: 1px solid #ddd; }
|
|
.msg-assistant b { color: #333; }
|
|
.msg-error { background: #fff5f5; border: 1px solid #e88; color: #a00; }
|
|
.msg-thinking { background: #fafafa; border: 1px solid #e5e5e5; color: #888; font-style: italic; }
|
|
.tool-call {
|
|
background: #f8f9fa; border: 1px solid #e0e0e0; border-radius: 8px;
|
|
margin-bottom: 0.6em; font-size: 0.93em; overflow: hidden;
|
|
}
|
|
.tool-header {
|
|
padding: 0.5em 1em; cursor: pointer; display: flex; align-items: center;
|
|
gap: 0.5em; user-select: none; font-weight: 600; color: #555;
|
|
}
|
|
.tool-header:hover { background: #f0f0f0; }
|
|
.tool-header .arrow { transition: transform 0.2s; display: inline-block; }
|
|
.tool-header .arrow.open { transform: rotate(90deg); }
|
|
.tool-body { padding: 0 1em 0.8em 1em; display: none; }
|
|
.tool-body.open { display: block; }
|
|
.tool-body pre {
|
|
background: #f5f5f5; padding: 0.6em; border-radius: 4px;
|
|
overflow-x: auto; font-size: 0.9em; margin: 0.4em 0;
|
|
}
|
|
.tool-body .tool-label { font-weight: 600; color: #666; font-size: 0.85em; margin-top: 0.5em; }
|
|
.tool-status { font-size: 0.8em; font-weight: normal; margin-left: auto; }
|
|
.tool-status.ok { color: #2a2; }
|
|
.tool-status.err { color: #c00; }
|
|
#prompt-bar {
|
|
display: flex; gap: 0.5em; align-items: center;
|
|
padding: 0.8em 0 0 0; border-top: 1px solid #eee;
|
|
}
|
|
#prompt-bar input {
|
|
flex: 1; margin-bottom: 0; padding: 0.6em 0.8em;
|
|
border: 1px solid #ccc; border-radius: 6px; font-size: 1em;
|
|
}
|
|
#prompt-bar button { padding: 0.6em 1.5em; border-radius: 6px; font-size: 1em; white-space: nowrap; }
|
|
#prompt-bar button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
.tools-bar {
|
|
display: flex; gap: 0.5em; align-items: center;
|
|
padding: 0.5em 0; font-size: 0.85em; color: #888;
|
|
}
|
|
.tools-bar .tool-count { font-weight: 600; color: #555; }
|
|
.settings-toggle {
|
|
background: none; border: 1px solid #ddd; border-radius: 6px;
|
|
padding: 0.3em 0.8em; font-size: 0.85em; cursor: pointer; color: #555;
|
|
}
|
|
.settings-toggle:hover { background: #f5f5f5; }
|
|
#settings-panel {
|
|
display: none; background: #fafafa; border: 1px solid #e5e5e5;
|
|
border-radius: 8px; padding: 1em 1.2em; margin-bottom: 1em;
|
|
}
|
|
#settings-panel.open { display: block; }
|
|
#settings-panel label { display: block; font-weight: 600; margin-top: 0.6em; font-size: 0.9em; }
|
|
#settings-panel input, #settings-panel select {
|
|
width: 100%; padding: 0.4em 0.6em; font-size: 0.9em;
|
|
border: 1px solid #ddd; border-radius: 4px; margin-top: 0.2em;
|
|
}
|
|
#settings-panel .settings-row { display: flex; gap: 1em; }
|
|
#settings-panel .settings-row > div { flex: 1; }
|
|
.clear-btn {
|
|
background: none; border: none; color: #999; cursor: pointer;
|
|
font-size: 0.85em; padding: 0.3em 0.5em;
|
|
}
|
|
.clear-btn:hover { color: #c00; }
|
|
.empty-state {
|
|
display: flex; flex-direction: column; align-items: center;
|
|
justify-content: center; height: 100%; color: #aaa; text-align: center;
|
|
}
|
|
.empty-state .icon { font-size: 3em; margin-bottom: 0.3em; }
|
|
.empty-state p { margin: 0.3em 0; max-width: 400px; }
|
|
</style>
|
|
|
|
<div id="agent-chat">
|
|
<div class="tools-bar">
|
|
<span>Tools: <span class="tool-count" id="tool-count">...</span></span>
|
|
<button class="settings-toggle" onclick="toggleSettings()">Settings</button>
|
|
<button class="clear-btn" onclick="clearChat()">Clear chat</button>
|
|
</div>
|
|
|
|
<div id="settings-panel">
|
|
<div class="settings-row">
|
|
<div>
|
|
<label>Provider</label>
|
|
<select id="provider">
|
|
<option value="openai">OpenAI</option>
|
|
<option value="anthropic">Anthropic</option>
|
|
<option value="gemini">Google Gemini</option>
|
|
<option value="atlascloud">Atlas Cloud</option>
|
|
<option value="groq">Groq</option>
|
|
<option value="mistral">Mistral</option>
|
|
<option value="together">Together AI</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label>Model (optional)</label>
|
|
<input type="text" id="model-name" placeholder="e.g. gpt-4o, claude-sonnet-4-20250514">
|
|
</div>
|
|
</div>
|
|
<label>API Key</label>
|
|
<input type="password" id="api-key" placeholder="sk-... or API key">
|
|
<label>Base URL (optional)</label>
|
|
<input type="text" id="base-url" placeholder="Leave blank for default">
|
|
<div style="margin-top:0.8em; display:flex; gap:0.5em; align-items:center;">
|
|
<button onclick="saveSettings()" style="padding:0.4em 1em; font-size:0.9em;">Save</button>
|
|
<span id="settings-status" style="color:#888; font-size:0.85em;"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="agent-messages">
|
|
<div class="empty-state" id="empty-state">
|
|
<div class="icon">🤖</div>
|
|
<p><b>Chat with your services</b></p>
|
|
<p>Ask the agent to interact with your microservices. It will discover and call the right tools automatically.</p>
|
|
<p id="empty-tools" style="font-size:0.85em; color:#bbb; margin-top:0.8em;"></p>
|
|
<div id="setup-hint" style="margin-top:1.2em; padding:1em; background:#f8f9fa; border-radius:8px; border:1px solid #e5e5e5; text-align:left; font-size:0.88em; max-width:400px; margin-left:auto; margin-right:auto;">
|
|
<p style="margin:0 0 0.5em; font-weight:600;">Getting started:</p>
|
|
<p style="margin:0 0 0.3em;">1. Click <b>Settings</b> above</p>
|
|
<p style="margin:0 0 0.3em;">2. Choose a provider and enter your API key</p>
|
|
<p style="margin:0 0 0.3em;">3. Type a prompt like <em>"list all services"</em></p>
|
|
<p style="margin:0.6em 0 0; color:#888; font-size:0.9em;">Or use the CLI: <code style="background:#e9ecef; padding:2px 5px; border-radius:3px;">micro chat --provider anthropic</code></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="prompt-bar">
|
|
<input type="text" id="prompt-input" placeholder="Ask the agent to call your services..." autofocus>
|
|
<button id="prompt-btn" onclick="sendPrompt()">Send</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function() {
|
|
var tools = [];
|
|
var toolCount = document.getElementById('tool-count');
|
|
|
|
loadSettings();
|
|
loadTools();
|
|
|
|
function loadSettings() {
|
|
fetch('/api/agent/settings')
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
if (data.provider) document.getElementById('provider').value = data.provider;
|
|
if (data.api_key) document.getElementById('api-key').value = data.api_key;
|
|
if (data.model) document.getElementById('model-name').value = data.model;
|
|
if (data.base_url) document.getElementById('base-url').value = data.base_url;
|
|
// Hide setup hint if API key is configured
|
|
if (data.api_key) {
|
|
var hint = document.getElementById('setup-hint');
|
|
if (hint) hint.style.display = 'none';
|
|
}
|
|
// Auto-show settings if no API key configured
|
|
if (!data.api_key) {
|
|
document.getElementById('settings-panel').classList.add('open');
|
|
}
|
|
})
|
|
.catch(function() {
|
|
document.getElementById('settings-panel').classList.add('open');
|
|
});
|
|
}
|
|
|
|
window.toggleSettings = function() {
|
|
document.getElementById('settings-panel').classList.toggle('open');
|
|
};
|
|
|
|
window.saveSettings = function() {
|
|
var status = document.getElementById('settings-status');
|
|
status.textContent = 'Saving...';
|
|
fetch('/api/agent/settings', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({
|
|
provider: document.getElementById('provider').value,
|
|
api_key: document.getElementById('api-key').value,
|
|
model: document.getElementById('model-name').value,
|
|
base_url: document.getElementById('base-url').value
|
|
})
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function() {
|
|
status.textContent = 'Saved';
|
|
setTimeout(function() { status.textContent = ''; }, 2000);
|
|
})
|
|
.catch(function(err) { status.textContent = 'Error: ' + err; });
|
|
};
|
|
|
|
function loadTools() {
|
|
fetch('/mcp/tools')
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
tools = data.tools || [];
|
|
toolCount.textContent = tools.length;
|
|
var emptyTools = document.getElementById('empty-tools');
|
|
if (tools.length > 0) {
|
|
var names = tools.slice(0, 5).map(function(t) { return t.name; });
|
|
var suffix = tools.length > 5 ? ' and ' + (tools.length - 5) + ' more' : '';
|
|
emptyTools.textContent = 'Available: ' + names.join(', ') + suffix;
|
|
} else {
|
|
emptyTools.textContent = 'No tools found. Start some services first.';
|
|
}
|
|
})
|
|
.catch(function() {
|
|
toolCount.textContent = '0';
|
|
});
|
|
}
|
|
|
|
window.clearChat = function() {
|
|
var container = document.getElementById('agent-messages');
|
|
container.innerHTML = '';
|
|
var emptyState = document.createElement('div');
|
|
emptyState.className = 'empty-state';
|
|
emptyState.id = 'empty-state';
|
|
emptyState.innerHTML = '<div class="icon">🤖</div><p><b>Chat with your services</b></p><p>Ask the agent to interact with your microservices.</p>';
|
|
container.appendChild(emptyState);
|
|
};
|
|
|
|
window.sendPrompt = function() {
|
|
var input = document.getElementById('prompt-input');
|
|
var text = input.value.trim();
|
|
if (!text) return;
|
|
input.value = '';
|
|
|
|
// Remove empty state on first message
|
|
var emptyState = document.getElementById('empty-state');
|
|
if (emptyState) emptyState.remove();
|
|
|
|
addMessage('user', escapeHtml(text));
|
|
|
|
// Show thinking indicator
|
|
var thinkingId = 'thinking-' + Date.now();
|
|
addMessage('thinking', 'Agent is thinking...', thinkingId);
|
|
|
|
var btn = document.getElementById('prompt-btn');
|
|
btn.disabled = true;
|
|
btn.textContent = 'Sending...';
|
|
|
|
var startTime = Date.now();
|
|
|
|
fetch('/api/agent/prompt', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({prompt: text})
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
var elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
btn.disabled = false;
|
|
btn.textContent = 'Send';
|
|
input.focus();
|
|
|
|
// Remove thinking indicator
|
|
var thinking = document.getElementById(thinkingId);
|
|
if (thinking) thinking.remove();
|
|
|
|
if (data.error) {
|
|
addMessage('error', escapeHtml(data.error));
|
|
return;
|
|
}
|
|
if (data.reply) {
|
|
addMessage('assistant', escapeHtml(data.reply));
|
|
}
|
|
if (data.tool_calls && data.tool_calls.length > 0) {
|
|
for (var i = 0; i < data.tool_calls.length; i++) {
|
|
var tc = data.tool_calls[i];
|
|
addToolCall(tc, elapsed);
|
|
}
|
|
}
|
|
if (data.answer) {
|
|
addMessage('assistant', escapeHtml(data.answer));
|
|
}
|
|
})
|
|
.catch(function(err) {
|
|
btn.disabled = false;
|
|
btn.textContent = 'Send';
|
|
input.focus();
|
|
var thinking = document.getElementById(thinkingId);
|
|
if (thinking) thinking.remove();
|
|
addMessage('error', 'Error: ' + escapeHtml(String(err)));
|
|
});
|
|
};
|
|
|
|
function addToolCall(tc, elapsed) {
|
|
var container = document.getElementById('agent-messages');
|
|
var div = document.createElement('div');
|
|
div.className = 'tool-call';
|
|
|
|
var hasError = tc.result && tc.result.error;
|
|
var statusClass = hasError ? 'err' : 'ok';
|
|
var statusText = hasError ? 'error' : elapsed + 's';
|
|
|
|
var inputJson = JSON.stringify(tc.input, null, 2);
|
|
var resultJson = JSON.stringify(tc.result, null, 2);
|
|
|
|
div.innerHTML =
|
|
'<div class="tool-header" onclick="this.querySelector(\'.arrow\').classList.toggle(\'open\'); this.nextElementSibling.classList.toggle(\'open\');">' +
|
|
'<span class="arrow">▶</span> ' +
|
|
'<code>' + escapeHtml(tc.tool) + '</code>' +
|
|
'<span class="tool-status ' + statusClass + '">' + statusText + '</span>' +
|
|
'</div>' +
|
|
'<div class="tool-body">' +
|
|
'<div class="tool-label">Input</div>' +
|
|
'<pre>' + escapeHtml(inputJson) + '</pre>' +
|
|
'<div class="tool-label">Result</div>' +
|
|
'<pre>' + escapeHtml(resultJson) + '</pre>' +
|
|
'</div>';
|
|
|
|
container.appendChild(div);
|
|
container.scrollTop = container.scrollHeight;
|
|
}
|
|
|
|
document.getElementById('prompt-input').addEventListener('keydown', function(e) {
|
|
if (e.key === 'Enter') { e.preventDefault(); window.sendPrompt(); }
|
|
});
|
|
|
|
function addMessage(type, html, id) {
|
|
var container = document.getElementById('agent-messages');
|
|
var div = document.createElement('div');
|
|
div.className = 'msg msg-' + type;
|
|
if (id) div.id = id;
|
|
if (type === 'user') {
|
|
div.innerHTML = '<b>You</b><br>' + html;
|
|
} else if (type === 'assistant' || type === 'answer') {
|
|
div.innerHTML = '<b>Agent</b><br>' + html;
|
|
} else if (type === 'thinking') {
|
|
div.innerHTML = html;
|
|
} else if (type === 'error') {
|
|
div.innerHTML = html;
|
|
}
|
|
container.appendChild(div);
|
|
container.scrollTop = container.scrollHeight;
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
var d = document.createElement('div');
|
|
d.textContent = str;
|
|
return d.innerHTML;
|
|
}
|
|
})();
|
|
</script>
|
|
{{end}}
|