/admin/mcp, for administrators only, since a server's credential reaches whatever it guards: each server's state, who has been granted its tools, its rate, the headers it is sent by name with the sealed ones marked, and its address; adding one with a bearer token or another header kept in the store; testing it with its credential and listing its tools; granting them to a team, project, org, person or key, written into that level's limits beside its other grants and its rate and budget; revoking; changing; replacing the credential; turning off and on; removing; and how a person points an agent at the gateway's /mcp with a key of their own. English and Turkish. The secret's field is named so the audit line redacts it. The admin API's server merge and test are package functions now, adminapi.MCPServerFrom and adminapi.TestMCPServer, shared by both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
104 lines
6.6 KiB
HTML
104 lines
6.6 KiB
HTML
{{define "admin_mcp"}}<!doctype html>
|
|
<html lang="{{lang}}"><head>
|
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>{{t "mcp.title"}} · llmgw</title><link rel="stylesheet" href="{{stylesheet}}">
|
|
<script defer src="{{asset "pages.js"}}"></script>
|
|
</head><body><div class="wrap">
|
|
<header>
|
|
<h1>{{t "mcp.title"}}</h1>
|
|
<p class="sub">{{th "mcp.sub"}}</p>
|
|
{{template "nav" .Nav}}
|
|
</header>
|
|
{{if .Error}}<p class="error" role="alert">{{.Error}}</p>{{end}}
|
|
{{if .Notice}}<p class="ok" role="status">{{.Notice}}</p>{{end}}
|
|
{{if .Tools}}<ul class="mcp-tools">{{range .Tools}}<li><code>{{.}}</code></li>{{end}}</ul>{{end}}
|
|
|
|
{{if .Servers}}
|
|
<table class="rows">
|
|
<tr><th>{{t "mcp.col.server"}}</th><th>{{t "mcp.col.state"}}</th><th>{{t "mcp.col.granted"}}</th><th class="num">{{t "mcp.col.rate"}}</th><th>{{t "mcp.col.sent"}}</th><th>{{t "mcp.col.address"}}</th></tr>
|
|
{{range .Servers}}<tr>
|
|
<td><strong>{{.Name}}</strong>{{with .Description}}<br><span class="opt">{{.}}</span>{{end}}</td>
|
|
<td>{{if .Enabled}}<span class="state state-ready">{{t "mcp.on"}}</span>{{else}}<span class="state">{{t "mcp.off"}}</span>{{end}}</td>
|
|
<td>{{if .Grants}}{{range $i, $g := .Grants}}{{if $i}}<br>{{end}}{{t (printf "mcp.scope.%s" $g.Scope)}} {{$g.Label}}: <code>{{range $j, $t := $g.Tools}}{{if $j}}, {{end}}{{$t}}{{end}}</code>{{end}}{{else}}<span class="cannot">{{t "mcp.nobody"}}</span>{{end}}</td>
|
|
<td class="num">{{if .RatePerMinute}}{{t "mcp.per_minute" .RatePerMinute}}{{else}}—{{end}}</td>
|
|
<td>{{range .Secret}}<code>{{.}}</code> <span class="opt">{{t "mcp.sealed"}}</span><br>{{end}}{{range .Plain}}<code>{{.}}</code><br>{{end}}{{if and (not .Secret) (not .Plain)}}—{{end}}</td>
|
|
<td><code>{{.URL}}</code></td>
|
|
</tr>
|
|
<tr><td colspan="6">
|
|
<details><summary>{{t "mcp.actions" .Name}}</summary>
|
|
<div class="mcp-actions">
|
|
<form method="post" action="/admin/mcp/{{.Name}}/test" class="inline">
|
|
<input type="hidden" name="csrf" value="{{$.CSRF}}"><button type="submit">{{t "mcp.test"}}</button></form>
|
|
{{if .Enabled}}
|
|
<form method="post" action="/admin/mcp/{{.Name}}/disable" class="inline" data-confirm="{{t "mcp.confirm_disable" .Name}}">
|
|
<input type="hidden" name="csrf" value="{{$.CSRF}}"><button type="submit">{{t "mcp.disable"}}</button></form>
|
|
{{else}}
|
|
<form method="post" action="/admin/mcp/{{.Name}}/enable" class="inline">
|
|
<input type="hidden" name="csrf" value="{{$.CSRF}}"><button type="submit">{{t "mcp.enable"}}</button></form>
|
|
{{end}}
|
|
<form method="post" action="/admin/mcp/{{.Name}}/remove" class="inline" data-confirm="{{t "mcp.confirm_remove" .Name}}">
|
|
<input type="hidden" name="csrf" value="{{$.CSRF}}"><button type="submit" class="link danger">{{t "mcp.remove"}}</button></form>
|
|
|
|
<h3>{{t "mcp.grant"}}</h3>
|
|
<form method="post" action="/admin/mcp/{{.Name}}/grant">
|
|
<input type="hidden" name="csrf" value="{{$.CSRF}}">
|
|
<label>{{t "mcp.to"}} <select name="scope">
|
|
<option value="team">{{t "mcp.scope.team"}}</option><option value="project">{{t "mcp.scope.project"}}</option>
|
|
<option value="org">{{t "mcp.scope.org"}}</option><option value="user">{{t "mcp.scope.user"}}</option><option value="key">{{t "mcp.scope.key"}}</option>
|
|
</select></label>
|
|
<label>{{t "mcp.which"}} <input type="text" name="which" placeholder="{{t "mcp.which_hint"}}" required></label>
|
|
<label>{{t "mcp.tools"}} <input type="text" name="tools" value="{{.Name}}.*"></label>
|
|
<button type="submit">{{t "mcp.grant_button"}}</button>
|
|
</form>
|
|
{{$name := .Name}}
|
|
{{range .Grants}}
|
|
<form method="post" action="/admin/mcp/{{$name}}/revoke" class="inline" data-confirm="{{t "mcp.confirm_revoke" $name .Label}}">
|
|
<input type="hidden" name="csrf" value="{{$.CSRF}}"><input type="hidden" name="scope" value="{{.Scope}}"><input type="hidden" name="id" value="{{.ID}}">
|
|
<button type="submit">{{t "mcp.revoke" .Label}}</button></form>
|
|
{{end}}
|
|
|
|
{{if not .Local}}
|
|
<h3>{{t "mcp.change"}}</h3>
|
|
<form method="post" action="/admin/mcp/{{.Name}}/edit">
|
|
<input type="hidden" name="csrf" value="{{$.CSRF}}">
|
|
<label>{{t "mcp.address"}} <input type="text" inputmode="url" name="url" value="{{.URL}}" required></label>
|
|
<label>{{t "mcp.about"}} <input type="text" name="description" value="{{.Description}}"></label>
|
|
<label>{{t "mcp.timeout"}} <input type="number" name="timeout" min="0" value="{{if .TimeoutSeconds}}{{.TimeoutSeconds}}{{end}}"></label>
|
|
<label>{{t "mcp.rate"}} <input type="number" name="rate" min="0" value="{{if .RatePerMinute}}{{.RatePerMinute}}{{end}}"></label>
|
|
<button type="submit">{{t "mcp.save"}}</button>
|
|
</form>
|
|
<h3>{{t "mcp.credential"}}</h3>
|
|
<form method="post" action="/admin/mcp/{{.Name}}/credential">
|
|
<input type="hidden" name="csrf" value="{{$.CSRF}}">
|
|
<label>{{t "mcp.auth"}} <select name="auth"><option value="bearer">{{t "mcp.auth.bearer"}}</option><option value="header">{{t "mcp.auth.header"}}</option></select></label>
|
|
<label>{{t "mcp.header"}} <input type="text" name="header"></label>
|
|
<label>{{t "mcp.secret"}} <input type="password" name="secret_value" autocomplete="new-password" required></label>
|
|
<button type="submit">{{t "mcp.replace"}}</button>
|
|
</form>
|
|
{{end}}
|
|
</div></details>
|
|
</td></tr>
|
|
{{end}}
|
|
</table>
|
|
{{else}}<p class="sub">{{t "mcp.none"}}</p>{{end}}
|
|
|
|
<h2>{{t "mcp.add"}}</h2>
|
|
<form method="post" action="/admin/mcp/add">
|
|
<input type="hidden" name="csrf" value="{{.CSRF}}">
|
|
<label>{{t "mcp.name"}} <input type="text" name="name" pattern="[a-z][a-z0-9_\-]{0,31}" required> <span class="opt">{{t "mcp.name_hint"}}</span></label>
|
|
<label>{{t "mcp.address"}} <input type="text" inputmode="url" name="url" required></label>
|
|
<label>{{t "mcp.about"}} <input type="text" name="description"></label>
|
|
<label>{{t "mcp.auth"}} <select name="auth"><option value="bearer">{{t "mcp.auth.bearer"}}</option><option value="header">{{t "mcp.auth.header"}}</option><option value="none">{{t "mcp.auth.none"}}</option></select></label>
|
|
<label>{{t "mcp.header"}} <input type="text" name="header"> <span class="opt">{{t "mcp.header_hint"}}</span></label>
|
|
<label>{{t "mcp.secret"}} <input type="password" name="secret_value" autocomplete="new-password"> <span class="opt">{{t "mcp.secret_hint"}}</span></label>
|
|
<label>{{t "mcp.timeout"}} <input type="number" name="timeout" min="0"></label>
|
|
<label>{{t "mcp.rate"}} <input type="number" name="rate" min="0"></label>
|
|
<button type="submit">{{t "mcp.add_button"}}</button>
|
|
</form>
|
|
|
|
<h2>{{t "mcp.agents"}}</h2>
|
|
<p class="sub">{{th "mcp.agents_sub"}}</p>
|
|
<pre translate="no">claude mcp add --transport http company {{.Gateway}} --header "Authorization: Bearer sk-llmgw-…"</pre>
|
|
<footer>llmgw {{.Version}}</footer>
|
|
</div></body></html>{{end}}
|