mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 17:36:45 +08:00
* feat(mcp): report how many agents use each workspace MCP server The workspace MCP library listing now carries agent_count: the number of live (non-archived) agents each entry is assigned to. Settings shows it so an admin can tell whether replacing or removing a server affects anyone. The field is optional in the client schema, so an older server that omits it simply shows no count. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * feat(settings): regroup settings by scope and consolidate scattered pages Implements the MUL-7732 settings redesign: - Navigation is grouped by who a setting affects: Personal, the current workspace (with Issues & workflow and Connections & extensions sub-groups), and This device on desktop. Pages and sections carry a scope badge (account, this device, workspace), and members see a lock on pages only owners and admins can change plus a shared read-only notice. - Settings search in the nav indexes page and row titles and descriptions; opening a result scrolls to and highlights the row. - Preferences is one page instead of three tabs, with a searchable time zone picker, a segmented theme control and a field grid for the create-issue dialogs. - Code merges Repositories, GitHub and self-hosted Git. Repositories are read-only rows edited through dialogs; self-hosted Git connects in a dialog that ends on the one-time webhook secret. - The PR merge rule moves to Statuses & transitions as an automatic transition; Code only shows its current value. Built-in statuses show edit and archive as unavailable instead of refusing after a click. - Integrations becomes Messaging (IM channels only) with a breadcrumb on detail pages; Composio moves to Personal as Connected apps. - Members gets tabs, search, invite and share-link dialogs and a seat summary. MCP servers show assignment counts and explain how MCP, connected apps and plugins differ. Workspace General shows members plain values and changes the issue prefix through an explicit dialog. - Skill labels are managed from the Skills page; Settings > Labels keeps the issue catalog. - Successful saves no longer toast; failures still do. Old ?tab= values (github, repositories, integrations, issue, chat, labs, and the Composio OAuth callback) resolve to the new pages, since server redirects still use them. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * docs: point settings paths at the regrouped settings pages GitHub, repositories and self-hosted Git now live under Settings > Code, IM bots under Settings > Messaging, and the PR merge rule under Settings > Statuses & transitions. The GitHub guide also drops the old master switch step in favor of pausing from the GitHub row. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(settings): edit the PR merge rule on the Code page The "After PRs merge, move the issue to" rule sits with the other pull request rules on Code again instead of a separate Automatic transitions section. It applies to every code host, so pausing GitHub features does not lock it. The statuses page goes back to Issue Statuses, and its merge badge links to the rule on Code. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(settings): manage skill labels under Settings → Labels again The Skills page entry was buried at the bottom of the filter menu's Labels submenu. Settings → Labels switches between the issue and skill catalogs again, using the members page's segmented switch (now SettingsViewTabs), and the Skills page keeps its shortcut. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(settings): keep GitHub resume reachable after a disconnect Pausing GitHub features is a workspace setting, but the pause and resume item lived in a menu that only rendered with an installation. A workspace paused before disconnecting, or with github_enabled=false and no installation, lost the way back, and every PR switch stayed disabled. On deployments without a GitHub App it could not even reconnect, though Co-authored-by works without one. The menu now shows whenever GitHub is connected or paused, and only Disconnect requires an installation. The paused note shows in either state. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(settings): let the URL choose the open members list The members page read `section` only to initialize its list tab, so a search result for invitations or share links opened while already on the page changed the URL but not the list, and back/forward did nothing. With a router the open list is now derived from the URL, and switching lists replaces `section`. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * fix(settings): tell repositories on different ports apart The manual repository dialog reused the GitHub import's identity, which drops the port, so https://git.example.com:9443/acme/api.git read as a duplicate of the same path on :8443 and could not be saved. Non-default ports now stay in the identity; default ports, including 22 for ssh://, still compare equal to the scp-like form. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> * test(comments): stop the recipient-menu tests flaking under CI load The steering tests opened a recipient chip menu while the composer could still re-render its chips, from the previous send settling or the preview catching up. That closed the menu, and the menu steps also used the 1s default wait inside the 5s default test timeout. Under parallel load locally, about 1 in 20 runs failed the same way as CI. Menu choices now go through a helper that reopens the menu until the item shows. The remaining waits get the 5s the first steps already had, and each test gets 15s. 140 of 140 loaded runs pass. Co-Authored-By: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]> Co-authored-by: multica-agent <[email protected]>
394 lines
12 KiB
Go
394 lines
12 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.31.1
|
|
// source: workspace_mcp.sql
|
|
|
|
package db
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const addAgentMcpServer = `-- name: AddAgentMcpServer :exec
|
|
INSERT INTO agent_mcp_server (agent_id, server_id)
|
|
VALUES ($1, $2)
|
|
ON CONFLICT DO NOTHING
|
|
`
|
|
|
|
type AddAgentMcpServerParams struct {
|
|
AgentID pgtype.UUID `json:"agent_id"`
|
|
ServerID pgtype.UUID `json:"server_id"`
|
|
}
|
|
|
|
func (q *Queries) AddAgentMcpServer(ctx context.Context, arg AddAgentMcpServerParams) error {
|
|
_, err := q.db.Exec(ctx, addAgentMcpServer, arg.AgentID, arg.ServerID)
|
|
return err
|
|
}
|
|
|
|
const countWorkspaceMcpServerAgents = `-- name: CountWorkspaceMcpServerAgents :many
|
|
SELECT ams.server_id, COUNT(*)::bigint AS agent_count
|
|
FROM agent_mcp_server ams
|
|
JOIN workspace_mcp_server s ON s.id = ams.server_id
|
|
JOIN agent a ON a.id = ams.agent_id
|
|
WHERE s.workspace_id = $1 AND a.archived_at IS NULL
|
|
GROUP BY ams.server_id
|
|
`
|
|
|
|
type CountWorkspaceMcpServerAgentsRow struct {
|
|
ServerID pgtype.UUID `json:"server_id"`
|
|
AgentCount int64 `json:"agent_count"`
|
|
}
|
|
|
|
// How many live agents each library entry is assigned to, for the workspace
|
|
// settings list. Archived agents never receive the server, so they are not
|
|
// counted; servers with no assignment are simply absent from the result.
|
|
func (q *Queries) CountWorkspaceMcpServerAgents(ctx context.Context, workspaceID pgtype.UUID) ([]CountWorkspaceMcpServerAgentsRow, error) {
|
|
rows, err := q.db.Query(ctx, countWorkspaceMcpServerAgents, workspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []CountWorkspaceMcpServerAgentsRow{}
|
|
for rows.Next() {
|
|
var i CountWorkspaceMcpServerAgentsRow
|
|
if err := rows.Scan(&i.ServerID, &i.AgentCount); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const createWorkspaceMcpServer = `-- name: CreateWorkspaceMcpServer :one
|
|
INSERT INTO workspace_mcp_server (workspace_id, name, config, created_by)
|
|
VALUES ($1, $2, $3, $4)
|
|
RETURNING id, workspace_id, name, config, created_by, created_at, updated_at
|
|
`
|
|
|
|
type CreateWorkspaceMcpServerParams struct {
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Name string `json:"name"`
|
|
Config []byte `json:"config"`
|
|
CreatedBy pgtype.UUID `json:"created_by"`
|
|
}
|
|
|
|
func (q *Queries) CreateWorkspaceMcpServer(ctx context.Context, arg CreateWorkspaceMcpServerParams) (WorkspaceMcpServer, error) {
|
|
row := q.db.QueryRow(ctx, createWorkspaceMcpServer,
|
|
arg.WorkspaceID,
|
|
arg.Name,
|
|
arg.Config,
|
|
arg.CreatedBy,
|
|
)
|
|
var i WorkspaceMcpServer
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Config,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const deleteAgentMcpServersByServer = `-- name: DeleteAgentMcpServersByServer :exec
|
|
DELETE FROM agent_mcp_server WHERE server_id = $1
|
|
`
|
|
|
|
// Bindings carry no FK, so removing a library entry must sweep them explicitly
|
|
// in the same transaction as the delete.
|
|
func (q *Queries) DeleteAgentMcpServersByServer(ctx context.Context, serverID pgtype.UUID) error {
|
|
_, err := q.db.Exec(ctx, deleteAgentMcpServersByServer, serverID)
|
|
return err
|
|
}
|
|
|
|
const deleteWorkspaceMcpServer = `-- name: DeleteWorkspaceMcpServer :execrows
|
|
DELETE FROM workspace_mcp_server
|
|
WHERE id = $1 AND workspace_id = $2
|
|
`
|
|
|
|
type DeleteWorkspaceMcpServerParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) DeleteWorkspaceMcpServer(ctx context.Context, arg DeleteWorkspaceMcpServerParams) (int64, error) {
|
|
result, err := q.db.Exec(ctx, deleteWorkspaceMcpServer, arg.ID, arg.WorkspaceID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
const getWorkspaceMcpServer = `-- name: GetWorkspaceMcpServer :one
|
|
SELECT id, workspace_id, name, config, created_by, created_at, updated_at FROM workspace_mcp_server
|
|
WHERE id = $1 AND workspace_id = $2
|
|
`
|
|
|
|
type GetWorkspaceMcpServerParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
func (q *Queries) GetWorkspaceMcpServer(ctx context.Context, arg GetWorkspaceMcpServerParams) (WorkspaceMcpServer, error) {
|
|
row := q.db.QueryRow(ctx, getWorkspaceMcpServer, arg.ID, arg.WorkspaceID)
|
|
var i WorkspaceMcpServer
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Config,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const listAgentMcpServers = `-- name: ListAgentMcpServers :many
|
|
SELECT s.id, s.workspace_id, s.name, s.config, s.created_at, s.updated_at, ams.enabled
|
|
FROM workspace_mcp_server s
|
|
JOIN agent_mcp_server ams ON ams.server_id = s.id
|
|
WHERE ams.agent_id = $1
|
|
ORDER BY s.name ASC
|
|
`
|
|
|
|
type ListAgentMcpServersRow struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Name string `json:"name"`
|
|
Config []byte `json:"config"`
|
|
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
|
|
// Every workspace server bound to this agent, enabled or not, for the agent
|
|
// settings list. `enabled` comes from the binding.
|
|
func (q *Queries) ListAgentMcpServers(ctx context.Context, agentID pgtype.UUID) ([]ListAgentMcpServersRow, error) {
|
|
rows, err := q.db.Query(ctx, listAgentMcpServers, agentID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListAgentMcpServersRow{}
|
|
for rows.Next() {
|
|
var i ListAgentMcpServersRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Config,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.Enabled,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listEnabledAgentMcpServers = `-- name: ListEnabledAgentMcpServers :many
|
|
SELECT s.name, s.config
|
|
FROM workspace_mcp_server s
|
|
JOIN agent_mcp_server ams ON ams.server_id = s.id
|
|
WHERE ams.agent_id = $1 AND ams.enabled = TRUE
|
|
ORDER BY s.name ASC
|
|
`
|
|
|
|
type ListEnabledAgentMcpServersRow struct {
|
|
Name string `json:"name"`
|
|
Config []byte `json:"config"`
|
|
}
|
|
|
|
// Claim path: only bound AND enabled servers reach the runtime. An unbound
|
|
// workspace server is invisible to this agent — creating one gives it to
|
|
// nobody until someone adds it here.
|
|
func (q *Queries) ListEnabledAgentMcpServers(ctx context.Context, agentID pgtype.UUID) ([]ListEnabledAgentMcpServersRow, error) {
|
|
rows, err := q.db.Query(ctx, listEnabledAgentMcpServers, agentID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []ListEnabledAgentMcpServersRow{}
|
|
for rows.Next() {
|
|
var i ListEnabledAgentMcpServersRow
|
|
if err := rows.Scan(&i.Name, &i.Config); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listWorkspaceMcpServers = `-- name: ListWorkspaceMcpServers :many
|
|
SELECT id, workspace_id, name, config, created_by, created_at, updated_at FROM workspace_mcp_server
|
|
WHERE workspace_id = $1
|
|
ORDER BY name ASC
|
|
`
|
|
|
|
// The workspace's MCP library. Returns `config` because the callers that need
|
|
// it (claim assembly, the write path's read-modify-write) run server-side; the
|
|
// HTTP layer projects it down to the non-secret inventory.
|
|
func (q *Queries) ListWorkspaceMcpServers(ctx context.Context, workspaceID pgtype.UUID) ([]WorkspaceMcpServer, error) {
|
|
rows, err := q.db.Query(ctx, listWorkspaceMcpServers, workspaceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
items := []WorkspaceMcpServer{}
|
|
for rows.Next() {
|
|
var i WorkspaceMcpServer
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Config,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const lockWorkspaceMcpServerForShare = `-- name: LockWorkspaceMcpServerForShare :one
|
|
SELECT id FROM workspace_mcp_server
|
|
WHERE id = $1 AND workspace_id = $2
|
|
FOR SHARE
|
|
`
|
|
|
|
type LockWorkspaceMcpServerForShareParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
// Taken by the assignment writer before it inserts. A shared row lock
|
|
// conflicts with the exclusive lock DeleteWorkspaceMcpServer takes, so an
|
|
// assignment can never land in the window between that delete sweeping the
|
|
// bindings and committing — the junction has no FK to catch the orphan.
|
|
func (q *Queries) LockWorkspaceMcpServerForShare(ctx context.Context, arg LockWorkspaceMcpServerForShareParams) (pgtype.UUID, error) {
|
|
row := q.db.QueryRow(ctx, lockWorkspaceMcpServerForShare, arg.ID, arg.WorkspaceID)
|
|
var id pgtype.UUID
|
|
err := row.Scan(&id)
|
|
return id, err
|
|
}
|
|
|
|
const lockWorkspaceMcpServerForUpdate = `-- name: LockWorkspaceMcpServerForUpdate :one
|
|
SELECT id FROM workspace_mcp_server
|
|
WHERE id = $1 AND workspace_id = $2
|
|
FOR UPDATE
|
|
`
|
|
|
|
type LockWorkspaceMcpServerForUpdateParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
}
|
|
|
|
// The delete half of the same protocol: take the row exclusively first, so a
|
|
// concurrent assignment either lands before this transaction (and is swept) or
|
|
// waits and then finds the server gone.
|
|
func (q *Queries) LockWorkspaceMcpServerForUpdate(ctx context.Context, arg LockWorkspaceMcpServerForUpdateParams) (pgtype.UUID, error) {
|
|
row := q.db.QueryRow(ctx, lockWorkspaceMcpServerForUpdate, arg.ID, arg.WorkspaceID)
|
|
var id pgtype.UUID
|
|
err := row.Scan(&id)
|
|
return id, err
|
|
}
|
|
|
|
const removeAgentMcpServer = `-- name: RemoveAgentMcpServer :execrows
|
|
DELETE FROM agent_mcp_server
|
|
WHERE agent_id = $1 AND server_id = $2
|
|
`
|
|
|
|
type RemoveAgentMcpServerParams struct {
|
|
AgentID pgtype.UUID `json:"agent_id"`
|
|
ServerID pgtype.UUID `json:"server_id"`
|
|
}
|
|
|
|
func (q *Queries) RemoveAgentMcpServer(ctx context.Context, arg RemoveAgentMcpServerParams) (int64, error) {
|
|
result, err := q.db.Exec(ctx, removeAgentMcpServer, arg.AgentID, arg.ServerID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
const setAgentMcpServerEnabled = `-- name: SetAgentMcpServerEnabled :execrows
|
|
UPDATE agent_mcp_server
|
|
SET enabled = $3
|
|
WHERE agent_id = $1 AND server_id = $2
|
|
`
|
|
|
|
type SetAgentMcpServerEnabledParams struct {
|
|
AgentID pgtype.UUID `json:"agent_id"`
|
|
ServerID pgtype.UUID `json:"server_id"`
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
|
|
func (q *Queries) SetAgentMcpServerEnabled(ctx context.Context, arg SetAgentMcpServerEnabledParams) (int64, error) {
|
|
result, err := q.db.Exec(ctx, setAgentMcpServerEnabled, arg.AgentID, arg.ServerID, arg.Enabled)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
const updateWorkspaceMcpServer = `-- name: UpdateWorkspaceMcpServer :one
|
|
UPDATE workspace_mcp_server SET
|
|
name = COALESCE($3, name),
|
|
config = COALESCE($4, config),
|
|
updated_at = now()
|
|
WHERE id = $1 AND workspace_id = $2
|
|
RETURNING id, workspace_id, name, config, created_by, created_at, updated_at
|
|
`
|
|
|
|
type UpdateWorkspaceMcpServerParams struct {
|
|
ID pgtype.UUID `json:"id"`
|
|
WorkspaceID pgtype.UUID `json:"workspace_id"`
|
|
Name pgtype.Text `json:"name"`
|
|
Config []byte `json:"config"`
|
|
}
|
|
|
|
// Full replace of one entry. The name can change here (unlike the previous
|
|
// document model) because bindings key off the id, so a rename cannot orphan
|
|
// the agents that use it.
|
|
func (q *Queries) UpdateWorkspaceMcpServer(ctx context.Context, arg UpdateWorkspaceMcpServerParams) (WorkspaceMcpServer, error) {
|
|
row := q.db.QueryRow(ctx, updateWorkspaceMcpServer,
|
|
arg.ID,
|
|
arg.WorkspaceID,
|
|
arg.Name,
|
|
arg.Config,
|
|
)
|
|
var i WorkspaceMcpServer
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.WorkspaceID,
|
|
&i.Name,
|
|
&i.Config,
|
|
&i.CreatedBy,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|