Files
llmgw/internal/web/templates/admin_mcp.html
T
Ilker ManapandClaude Opus 5.5 03d3bcfd2d The company's MCP servers on a web page
/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>
2026-10-28 19:26:51 +01:00

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}}