CI / test (push) Successful in 9m15s
- Revoking a server's tools takes them from every level beneath too, and a level left naming no tool stays closed ([]) while any level above or beneath names tools: an organization's revoke left its teams the copies their first grants took, and going back to no list opened the level to what was above it. A closed level is shown on the page and in the console with an action to open it (store.OpenTools). A key's owner's list is among the levels above a key. - The flag check matches short flags as written again (-m, -hf, -mu were let through), takes a long flag's beginning for it only for the engines that parse with argparse (vLLM, SGLang, ComfyUI: --li is --listen), so llama.cpp's own --chat-template is not refused, and refuses llama.cpp's short spellings (-hfr, -hff, -mm, -jf, …). - A stored model with an adapter by Hugging Face id can be changed again: what it already had is not asked about anew. - The catalog reads a Hugging Face id as the agent does (engines.IsHubID). - The page's form cannot move llmgw:admin to another address. - server init skips the memory check only where the gateway already runs, not wherever a half-finished first run left server.yaml. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
110 lines
7.2 KiB
HTML
110 lines
7.2 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}}{{if .Revocable}}
|
|
<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}}{{end}}
|
|
|
|
<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{{if .Local}} readonly{{end}}></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>
|
|
{{if not .Local}}
|
|
<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}}
|
|
{{if .Closed}}<p class="note">{{t "mcp.closed"}}</p>
|
|
<ul class="mcp-closed">{{range .Closed}}<li>{{t (printf "mcp.scope.%s" .Scope)}} {{.Label}}
|
|
<form method="post" action="/admin/mcp/open" class="inline" data-confirm="{{t "mcp.confirm_open" .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" class="link">{{t "mcp.open"}}</button></form></li>{{end}}</ul>{{end}}
|
|
<p class="sub">{{t "mcp.how_grants"}}</p>
|
|
|
|
<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}}
|