You've already forked agentic-coding-workflow
633 lines
22 KiB
Go
633 lines
22 KiB
Go
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
|
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
|
|
|
|
"github.com/yan1h/agent-coding-workflow/internal/chat"
|
|
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
|
|
"github.com/yan1h/agent-coding-workflow/internal/project"
|
|
)
|
|
|
|
// registerChatTools 注册 chat 相关 MCP 工具:会话 CRUD、发消息、消息查询、
|
|
// prompt 模板 CRUD、模型列表。
|
|
//
|
|
// 范围边界(spec 决策):LLM endpoint/model 管理、用量统计、附件上传、
|
|
// 流式接收(SSE)与 cancel/retry(强依赖流式上下文)不经 MCP 暴露。
|
|
// send_message 的 assistant 回复异步生成,调用方用 list_recent_messages 轮询。
|
|
func registerChatTools(srv *mcpsdk.Server, deps ServerDeps) {
|
|
registerListRecentMessages(srv, deps)
|
|
registerListConversations(srv, deps)
|
|
registerGetConversation(srv, deps)
|
|
registerCreateConversation(srv, deps)
|
|
registerUpdateConversationTitle(srv, deps)
|
|
registerDeleteConversation(srv, deps)
|
|
registerSendMessage(srv, deps)
|
|
registerListPromptTemplates(srv, deps)
|
|
registerCreatePromptTemplate(srv, deps)
|
|
registerUpdatePromptTemplate(srv, deps)
|
|
registerDeletePromptTemplate(srv, deps)
|
|
registerListModels(srv, deps)
|
|
}
|
|
|
|
// ===== 共用 DTO / helper =====
|
|
|
|
type conversationDetail struct {
|
|
ID string `json:"id"`
|
|
Title string `json:"title,omitempty"`
|
|
ModelID string `json:"model_id"`
|
|
ProjectID string `json:"project_id,omitempty"`
|
|
WorkspaceID string `json:"workspace_id,omitempty"`
|
|
IssueID string `json:"issue_id,omitempty"`
|
|
RequirementID string `json:"requirement_id,omitempty"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
func conversationDetailFrom(cv *chat.Conversation) conversationDetail {
|
|
d := conversationDetail{
|
|
ID: cv.ID.String(), ModelID: cv.ModelID.String(),
|
|
CreatedAt: cv.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
|
UpdatedAt: cv.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
|
}
|
|
if cv.Title != nil {
|
|
d.Title = *cv.Title
|
|
}
|
|
if cv.ProjectID != nil {
|
|
d.ProjectID = cv.ProjectID.String()
|
|
}
|
|
if cv.WorkspaceID != nil {
|
|
d.WorkspaceID = cv.WorkspaceID.String()
|
|
}
|
|
if cv.IssueID != nil {
|
|
d.IssueID = cv.IssueID.String()
|
|
}
|
|
if cv.RequirementID != nil {
|
|
d.RequirementID = cv.RequirementID.String()
|
|
}
|
|
return d
|
|
}
|
|
|
|
// mountRefs 是会话挂载维度的 UUID 集合(nil = 未挂载)。
|
|
type mountRefs struct {
|
|
ProjectID *uuid.UUID
|
|
WorkspaceID *uuid.UUID
|
|
IssueID *uuid.UUID
|
|
RequirementID *uuid.UUID
|
|
}
|
|
|
|
// resolveMountRefs 把 MCP 友好的挂载入参(project_slug + 编号)解析为 UUID,
|
|
// 并完成 project scope 校验。requirement/issue 编号必须随 project_slug 一起给。
|
|
func resolveMountRefs(ctx context.Context, deps ServerDeps, c project.Caller,
|
|
projectSlug, workspaceID string, requirementNumber, issueNumber int) (mountRefs, error) {
|
|
var refs mountRefs
|
|
if projectSlug == "" && (requirementNumber > 0 || issueNumber > 0) {
|
|
return refs, errs.New(errs.CodeInvalidInput,
|
|
"project_slug is required when requirement_number/issue_number is set")
|
|
}
|
|
if projectSlug != "" {
|
|
p, err := deps.Projects.Get(ctx, c, projectSlug)
|
|
if err != nil {
|
|
return refs, err
|
|
}
|
|
if err := CheckProjectFromCtx(ctx, p.ID); err != nil {
|
|
return refs, err
|
|
}
|
|
pid := p.ID
|
|
refs.ProjectID = &pid
|
|
if requirementNumber > 0 {
|
|
r, err := deps.Reqs.Get(ctx, c, projectSlug, requirementNumber)
|
|
if err != nil {
|
|
return refs, err
|
|
}
|
|
rid := r.ID
|
|
refs.RequirementID = &rid
|
|
}
|
|
if issueNumber > 0 {
|
|
i, err := deps.Issues.Get(ctx, c, projectSlug, issueNumber)
|
|
if err != nil {
|
|
return refs, err
|
|
}
|
|
iid := i.ID
|
|
refs.IssueID = &iid
|
|
}
|
|
}
|
|
if workspaceID != "" {
|
|
w, _, err := scopedWorkspace(ctx, deps, c, workspaceID)
|
|
if err != nil {
|
|
return refs, err
|
|
}
|
|
wid := w.ID
|
|
refs.WorkspaceID = &wid
|
|
}
|
|
return refs, nil
|
|
}
|
|
|
|
// ===== list_recent_messages =====
|
|
|
|
type listRecentMessagesIn struct {
|
|
ConversationID string `json:"conversation_id" jsonschema:"UUID of the conversation"`
|
|
BeforeID *int64 `json:"before_id,omitempty" jsonschema:"cursor: return messages with ID < this value"`
|
|
Limit int `json:"limit,omitempty" jsonschema:"max messages to return (default 50, max 200)"`
|
|
}
|
|
type messageSummary struct {
|
|
ID int64 `json:"id"`
|
|
Role string `json:"role"`
|
|
Content string `json:"content"`
|
|
Status string `json:"status"`
|
|
CreatedAt string `json:"created_at"`
|
|
}
|
|
type listRecentMessagesOut struct {
|
|
Messages []messageSummary `json:"messages"`
|
|
}
|
|
|
|
func messageSummaryFrom(m chat.Message) messageSummary {
|
|
return messageSummary{
|
|
ID: m.ID,
|
|
Role: string(m.Role),
|
|
Content: m.Content,
|
|
Status: string(m.Status),
|
|
CreatedAt: m.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
|
}
|
|
}
|
|
|
|
func registerListRecentMessages(srv *mcpsdk.Server, deps ServerDeps) {
|
|
mcpsdk.AddTool(srv,
|
|
&mcpsdk.Tool{Name: "list_recent_messages", Description: "List recent chat messages in a conversation."},
|
|
func(ctx context.Context, _ *mcpsdk.CallToolRequest, in listRecentMessagesIn) (*mcpsdk.CallToolResult, listRecentMessagesOut, error) {
|
|
if err := CheckToolFromCtx(ctx, "list_recent_messages"); err != nil {
|
|
return nil, listRecentMessagesOut{}, err
|
|
}
|
|
c, err := deps.Caller.Resolve(ctx)
|
|
if err != nil {
|
|
return nil, listRecentMessagesOut{}, err
|
|
}
|
|
|
|
convID, err := uuid.Parse(in.ConversationID)
|
|
if err != nil {
|
|
return nil, listRecentMessagesOut{}, fmt.Errorf("invalid conversation_id: %w", err)
|
|
}
|
|
|
|
limit := in.Limit
|
|
if limit <= 0 {
|
|
limit = 50
|
|
}
|
|
if limit > 200 {
|
|
limit = 200
|
|
}
|
|
|
|
msgs, err := deps.Messages.ListMessages(ctx, c.UserID, convID, in.BeforeID, limit)
|
|
if err != nil {
|
|
return nil, listRecentMessagesOut{}, err
|
|
}
|
|
|
|
out := listRecentMessagesOut{Messages: make([]messageSummary, 0, len(msgs))}
|
|
for _, m := range msgs {
|
|
out.Messages = append(out.Messages, messageSummaryFrom(m))
|
|
}
|
|
return nil, out, nil
|
|
})
|
|
}
|
|
|
|
// ===== list_conversations =====
|
|
|
|
type listConversationsIn struct {
|
|
ProjectSlug string `json:"project_slug,omitempty" jsonschema:"filter by project"`
|
|
WorkspaceID string `json:"workspace_id,omitempty" jsonschema:"filter by workspace UUID"`
|
|
RequirementNumber int `json:"requirement_number,omitempty" jsonschema:"filter by requirement (needs project_slug)"`
|
|
IssueNumber int `json:"issue_number,omitempty" jsonschema:"filter by issue (needs project_slug)"`
|
|
TitleQuery string `json:"title_query,omitempty" jsonschema:"fuzzy title filter"`
|
|
BeforeID string `json:"before_id,omitempty" jsonschema:"cursor: conversation UUID"`
|
|
Limit int `json:"limit,omitempty" jsonschema:"max conversations to return (default 50, max 200)"`
|
|
}
|
|
type listConversationsOut struct {
|
|
Conversations []conversationDetail `json:"conversations"`
|
|
}
|
|
|
|
func registerListConversations(srv *mcpsdk.Server, deps ServerDeps) {
|
|
mcpsdk.AddTool(srv,
|
|
&mcpsdk.Tool{Name: "list_conversations", Description: "List the caller's chat conversations."},
|
|
func(ctx context.Context, _ *mcpsdk.CallToolRequest, in listConversationsIn) (*mcpsdk.CallToolResult, listConversationsOut, error) {
|
|
if err := CheckToolFromCtx(ctx, "list_conversations"); err != nil {
|
|
return nil, listConversationsOut{}, err
|
|
}
|
|
c, err := deps.Caller.Resolve(ctx)
|
|
if err != nil {
|
|
return nil, listConversationsOut{}, err
|
|
}
|
|
refs, err := resolveMountRefs(ctx, deps, c, in.ProjectSlug, in.WorkspaceID, in.RequirementNumber, in.IssueNumber)
|
|
if err != nil {
|
|
return nil, listConversationsOut{}, err
|
|
}
|
|
filter := chat.ConversationFilter{
|
|
ProjectID: refs.ProjectID, WorkspaceID: refs.WorkspaceID,
|
|
IssueID: refs.IssueID, RequirementID: refs.RequirementID,
|
|
TitleQuery: in.TitleQuery,
|
|
}
|
|
limit := in.Limit
|
|
if limit <= 0 {
|
|
limit = 50
|
|
}
|
|
if limit > 200 {
|
|
limit = 200
|
|
}
|
|
filter.Limit = limit
|
|
if in.BeforeID != "" {
|
|
bid, err := uuid.Parse(in.BeforeID)
|
|
if err != nil {
|
|
return nil, listConversationsOut{}, errs.Wrap(err, errs.CodeInvalidInput, "before_id parse")
|
|
}
|
|
filter.BeforeID = &bid
|
|
}
|
|
cvs, err := deps.Conversations.List(ctx, c.UserID, filter)
|
|
if err != nil {
|
|
return nil, listConversationsOut{}, err
|
|
}
|
|
out := listConversationsOut{Conversations: make([]conversationDetail, 0, len(cvs))}
|
|
for i := range cvs {
|
|
out.Conversations = append(out.Conversations, conversationDetailFrom(&cvs[i]))
|
|
}
|
|
return nil, out, nil
|
|
})
|
|
}
|
|
|
|
// ===== get_conversation =====
|
|
|
|
type getConversationIn struct {
|
|
ConversationID string `json:"conversation_id" jsonschema:"UUID of the conversation"`
|
|
Limit int `json:"limit,omitempty" jsonschema:"max recent messages to include (default 50, max 200)"`
|
|
}
|
|
type getConversationOut struct {
|
|
Conversation conversationDetail `json:"conversation"`
|
|
Messages []messageSummary `json:"messages"`
|
|
}
|
|
|
|
func registerGetConversation(srv *mcpsdk.Server, deps ServerDeps) {
|
|
mcpsdk.AddTool(srv,
|
|
&mcpsdk.Tool{Name: "get_conversation", Description: "Get a conversation with its recent messages."},
|
|
func(ctx context.Context, _ *mcpsdk.CallToolRequest, in getConversationIn) (*mcpsdk.CallToolResult, getConversationOut, error) {
|
|
if err := CheckToolFromCtx(ctx, "get_conversation"); err != nil {
|
|
return nil, getConversationOut{}, err
|
|
}
|
|
c, err := deps.Caller.Resolve(ctx)
|
|
if err != nil {
|
|
return nil, getConversationOut{}, err
|
|
}
|
|
convID, err := uuid.Parse(in.ConversationID)
|
|
if err != nil {
|
|
return nil, getConversationOut{}, errs.Wrap(err, errs.CodeInvalidInput, "conversation_id parse")
|
|
}
|
|
limit := in.Limit
|
|
if limit <= 0 {
|
|
limit = 50
|
|
}
|
|
if limit > 200 {
|
|
limit = 200
|
|
}
|
|
cv, msgs, err := deps.Conversations.Get(ctx, c.UserID, convID, limit)
|
|
if err != nil {
|
|
return nil, getConversationOut{}, err
|
|
}
|
|
out := getConversationOut{
|
|
Conversation: conversationDetailFrom(cv),
|
|
Messages: make([]messageSummary, 0, len(msgs)),
|
|
}
|
|
for _, m := range msgs {
|
|
out.Messages = append(out.Messages, messageSummaryFrom(m))
|
|
}
|
|
return nil, out, nil
|
|
})
|
|
}
|
|
|
|
// ===== create_conversation =====
|
|
|
|
type createConversationIn struct {
|
|
ModelID string `json:"model_id" jsonschema:"LLM model UUID (see list_models)"`
|
|
TemplateID string `json:"template_id,omitempty" jsonschema:"prompt template UUID"`
|
|
SystemPrompt string `json:"system_prompt,omitempty" jsonschema:"custom system prompt (overrides template)"`
|
|
ProjectSlug string `json:"project_slug,omitempty" jsonschema:"mount to a project"`
|
|
WorkspaceID string `json:"workspace_id,omitempty" jsonschema:"mount to a workspace UUID"`
|
|
RequirementNumber int `json:"requirement_number,omitempty" jsonschema:"mount to a requirement (needs project_slug)"`
|
|
IssueNumber int `json:"issue_number,omitempty" jsonschema:"mount to an issue (needs project_slug)"`
|
|
}
|
|
type conversationCreateOut struct {
|
|
OK bool `json:"ok"`
|
|
Conversation conversationDetail `json:"conversation"`
|
|
}
|
|
|
|
func registerCreateConversation(srv *mcpsdk.Server, deps ServerDeps) {
|
|
mcpsdk.AddTool(srv,
|
|
&mcpsdk.Tool{Name: "create_conversation", Description: "Create a new chat conversation."},
|
|
func(ctx context.Context, _ *mcpsdk.CallToolRequest, in createConversationIn) (*mcpsdk.CallToolResult, conversationCreateOut, error) {
|
|
if err := CheckToolFromCtx(ctx, "create_conversation"); err != nil {
|
|
return nil, conversationCreateOut{}, err
|
|
}
|
|
c, err := deps.Caller.Resolve(ctx)
|
|
if err != nil {
|
|
return nil, conversationCreateOut{}, err
|
|
}
|
|
modelID, err := uuid.Parse(in.ModelID)
|
|
if err != nil {
|
|
return nil, conversationCreateOut{}, errs.Wrap(err, errs.CodeInvalidInput, "model_id parse")
|
|
}
|
|
refs, err := resolveMountRefs(ctx, deps, c, in.ProjectSlug, in.WorkspaceID, in.RequirementNumber, in.IssueNumber)
|
|
if err != nil {
|
|
return nil, conversationCreateOut{}, err
|
|
}
|
|
input := chat.CreateConversationInput{
|
|
ModelID: modelID,
|
|
ProjectID: refs.ProjectID, WorkspaceID: refs.WorkspaceID,
|
|
IssueID: refs.IssueID, RequirementID: refs.RequirementID,
|
|
}
|
|
if in.TemplateID != "" {
|
|
tid, err := uuid.Parse(in.TemplateID)
|
|
if err != nil {
|
|
return nil, conversationCreateOut{}, errs.Wrap(err, errs.CodeInvalidInput, "template_id parse")
|
|
}
|
|
input.TemplateID = &tid
|
|
}
|
|
if in.SystemPrompt != "" {
|
|
input.SystemPrompt = &in.SystemPrompt
|
|
}
|
|
cv, err := deps.Conversations.Create(ctx, c.UserID, input)
|
|
if err != nil {
|
|
return nil, conversationCreateOut{}, err
|
|
}
|
|
return nil, conversationCreateOut{OK: true, Conversation: conversationDetailFrom(cv)}, nil
|
|
})
|
|
}
|
|
|
|
// ===== update_conversation_title =====
|
|
|
|
type updateConversationTitleIn struct {
|
|
ConversationID string `json:"conversation_id" jsonschema:"UUID of the conversation"`
|
|
Title string `json:"title" jsonschema:"new title (non-empty)"`
|
|
}
|
|
|
|
func registerUpdateConversationTitle(srv *mcpsdk.Server, deps ServerDeps) {
|
|
mcpsdk.AddTool(srv,
|
|
&mcpsdk.Tool{Name: "update_conversation_title", Description: "Rename a conversation."},
|
|
func(ctx context.Context, _ *mcpsdk.CallToolRequest, in updateConversationTitleIn) (*mcpsdk.CallToolResult, okOut, error) {
|
|
if err := CheckToolFromCtx(ctx, "update_conversation_title"); err != nil {
|
|
return nil, okOut{}, err
|
|
}
|
|
c, err := deps.Caller.Resolve(ctx)
|
|
if err != nil {
|
|
return nil, okOut{}, err
|
|
}
|
|
convID, err := uuid.Parse(in.ConversationID)
|
|
if err != nil {
|
|
return nil, okOut{}, errs.Wrap(err, errs.CodeInvalidInput, "conversation_id parse")
|
|
}
|
|
if err := deps.Conversations.UpdateTitle(ctx, c.UserID, convID, in.Title); err != nil {
|
|
return nil, okOut{}, err
|
|
}
|
|
return nil, okOut{OK: true}, nil
|
|
})
|
|
}
|
|
|
|
// ===== delete_conversation =====
|
|
|
|
type deleteConversationIn struct {
|
|
ConversationID string `json:"conversation_id" jsonschema:"UUID of the conversation"`
|
|
}
|
|
|
|
func registerDeleteConversation(srv *mcpsdk.Server, deps ServerDeps) {
|
|
mcpsdk.AddTool(srv,
|
|
&mcpsdk.Tool{Name: "delete_conversation", Description: "Soft-delete a conversation."},
|
|
func(ctx context.Context, _ *mcpsdk.CallToolRequest, in deleteConversationIn) (*mcpsdk.CallToolResult, okOut, error) {
|
|
if err := CheckToolFromCtx(ctx, "delete_conversation"); err != nil {
|
|
return nil, okOut{}, err
|
|
}
|
|
c, err := deps.Caller.Resolve(ctx)
|
|
if err != nil {
|
|
return nil, okOut{}, err
|
|
}
|
|
convID, err := uuid.Parse(in.ConversationID)
|
|
if err != nil {
|
|
return nil, okOut{}, errs.Wrap(err, errs.CodeInvalidInput, "conversation_id parse")
|
|
}
|
|
if err := deps.Conversations.SoftDelete(ctx, c.UserID, convID); err != nil {
|
|
return nil, okOut{}, err
|
|
}
|
|
return nil, okOut{OK: true}, nil
|
|
})
|
|
}
|
|
|
|
// ===== send_message =====
|
|
|
|
type sendMessageIn struct {
|
|
ConversationID string `json:"conversation_id" jsonschema:"UUID of the conversation"`
|
|
Content string `json:"content" jsonschema:"message content (non-empty)"`
|
|
}
|
|
type sendMessageOut struct {
|
|
OK bool `json:"ok"`
|
|
UserMessageID int64 `json:"user_message_id"`
|
|
AssistantMessageID int64 `json:"assistant_message_id"`
|
|
}
|
|
|
|
func registerSendMessage(srv *mcpsdk.Server, deps ServerDeps) {
|
|
mcpsdk.AddTool(srv,
|
|
&mcpsdk.Tool{Name: "send_message", Description: "Send a user message to a conversation. The assistant reply is generated asynchronously; poll list_recent_messages for the result (attachments are not supported via MCP)."},
|
|
func(ctx context.Context, _ *mcpsdk.CallToolRequest, in sendMessageIn) (*mcpsdk.CallToolResult, sendMessageOut, error) {
|
|
if err := CheckToolFromCtx(ctx, "send_message"); err != nil {
|
|
return nil, sendMessageOut{}, err
|
|
}
|
|
c, err := deps.Caller.Resolve(ctx)
|
|
if err != nil {
|
|
return nil, sendMessageOut{}, err
|
|
}
|
|
convID, err := uuid.Parse(in.ConversationID)
|
|
if err != nil {
|
|
return nil, sendMessageOut{}, errs.Wrap(err, errs.CodeInvalidInput, "conversation_id parse")
|
|
}
|
|
userMsgID, assistantMsgID, err := deps.Messages.Send(ctx, c.UserID, convID, in.Content, nil)
|
|
if err != nil {
|
|
return nil, sendMessageOut{}, err
|
|
}
|
|
return nil, sendMessageOut{OK: true, UserMessageID: userMsgID, AssistantMessageID: assistantMsgID}, nil
|
|
})
|
|
}
|
|
|
|
// ===== list_prompt_templates =====
|
|
|
|
type templateDetail struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Scope string `json:"scope"`
|
|
Content string `json:"content"`
|
|
}
|
|
type listPromptTemplatesOut struct {
|
|
Templates []templateDetail `json:"templates"`
|
|
}
|
|
|
|
func registerListPromptTemplates(srv *mcpsdk.Server, deps ServerDeps) {
|
|
mcpsdk.AddTool(srv,
|
|
&mcpsdk.Tool{Name: "list_prompt_templates", Description: "List prompt templates visible to the caller (system + own)."},
|
|
func(ctx context.Context, _ *mcpsdk.CallToolRequest, _ struct{}) (*mcpsdk.CallToolResult, listPromptTemplatesOut, error) {
|
|
if err := CheckToolFromCtx(ctx, "list_prompt_templates"); err != nil {
|
|
return nil, listPromptTemplatesOut{}, err
|
|
}
|
|
c, err := deps.Caller.Resolve(ctx)
|
|
if err != nil {
|
|
return nil, listPromptTemplatesOut{}, err
|
|
}
|
|
ts, err := deps.Templates.List(ctx, c.UserID)
|
|
if err != nil {
|
|
return nil, listPromptTemplatesOut{}, err
|
|
}
|
|
out := listPromptTemplatesOut{Templates: make([]templateDetail, 0, len(ts))}
|
|
for _, t := range ts {
|
|
out.Templates = append(out.Templates, templateDetail{
|
|
ID: t.ID.String(), Name: t.Name, Scope: string(t.Scope), Content: t.Content,
|
|
})
|
|
}
|
|
return nil, out, nil
|
|
})
|
|
}
|
|
|
|
// ===== create_prompt_template =====
|
|
|
|
type createPromptTemplateIn struct {
|
|
Name string `json:"name" jsonschema:"template name (non-empty)"`
|
|
Content string `json:"content" jsonschema:"template content (non-empty)"`
|
|
Scope string `json:"scope,omitempty" jsonschema:"system|user (default user; system requires admin)"`
|
|
}
|
|
type templateCreateOut struct {
|
|
OK bool `json:"ok"`
|
|
Template templateDetail `json:"template"`
|
|
}
|
|
|
|
func registerCreatePromptTemplate(srv *mcpsdk.Server, deps ServerDeps) {
|
|
mcpsdk.AddTool(srv,
|
|
&mcpsdk.Tool{Name: "create_prompt_template", Description: "Create a prompt template. scope=system requires admin."},
|
|
func(ctx context.Context, _ *mcpsdk.CallToolRequest, in createPromptTemplateIn) (*mcpsdk.CallToolResult, templateCreateOut, error) {
|
|
if err := CheckToolFromCtx(ctx, "create_prompt_template"); err != nil {
|
|
return nil, templateCreateOut{}, err
|
|
}
|
|
c, err := deps.Caller.Resolve(ctx)
|
|
if err != nil {
|
|
return nil, templateCreateOut{}, err
|
|
}
|
|
scope := chat.TemplateScope(in.Scope)
|
|
if scope == "" {
|
|
scope = chat.TemplateScopeUser
|
|
}
|
|
// 与 HTTP handler 对齐:system 模板仅 admin 可建(service 层不做此校验)。
|
|
if scope == chat.TemplateScopeSystem && !c.IsAdmin {
|
|
return nil, templateCreateOut{}, errs.New(errs.CodeForbidden, "admin only: system-scoped template")
|
|
}
|
|
t, err := deps.Templates.Create(ctx, c.UserID, in.Name, in.Content, scope)
|
|
if err != nil {
|
|
return nil, templateCreateOut{}, err
|
|
}
|
|
return nil, templateCreateOut{OK: true, Template: templateDetail{
|
|
ID: t.ID.String(), Name: t.Name, Scope: string(t.Scope), Content: t.Content,
|
|
}}, nil
|
|
})
|
|
}
|
|
|
|
// ===== update_prompt_template =====
|
|
|
|
type updatePromptTemplateIn struct {
|
|
TemplateID string `json:"template_id" jsonschema:"template UUID"`
|
|
Name string `json:"name" jsonschema:"new name (non-empty)"`
|
|
Content string `json:"content" jsonschema:"new content (non-empty)"`
|
|
}
|
|
|
|
func registerUpdatePromptTemplate(srv *mcpsdk.Server, deps ServerDeps) {
|
|
mcpsdk.AddTool(srv,
|
|
&mcpsdk.Tool{Name: "update_prompt_template", Description: "Update a prompt template (owner or system template)."},
|
|
func(ctx context.Context, _ *mcpsdk.CallToolRequest, in updatePromptTemplateIn) (*mcpsdk.CallToolResult, okOut, error) {
|
|
if err := CheckToolFromCtx(ctx, "update_prompt_template"); err != nil {
|
|
return nil, okOut{}, err
|
|
}
|
|
c, err := deps.Caller.Resolve(ctx)
|
|
if err != nil {
|
|
return nil, okOut{}, err
|
|
}
|
|
tid, err := uuid.Parse(in.TemplateID)
|
|
if err != nil {
|
|
return nil, okOut{}, errs.Wrap(err, errs.CodeInvalidInput, "template_id parse")
|
|
}
|
|
if err := deps.Templates.Update(ctx, c.UserID, c.IsAdmin, tid, in.Name, in.Content); err != nil {
|
|
return nil, okOut{}, err
|
|
}
|
|
return nil, okOut{OK: true}, nil
|
|
})
|
|
}
|
|
|
|
// ===== delete_prompt_template =====
|
|
|
|
type deletePromptTemplateIn struct {
|
|
TemplateID string `json:"template_id" jsonschema:"template UUID"`
|
|
}
|
|
|
|
func registerDeletePromptTemplate(srv *mcpsdk.Server, deps ServerDeps) {
|
|
mcpsdk.AddTool(srv,
|
|
&mcpsdk.Tool{Name: "delete_prompt_template", Description: "Delete a prompt template (owner or system template)."},
|
|
func(ctx context.Context, _ *mcpsdk.CallToolRequest, in deletePromptTemplateIn) (*mcpsdk.CallToolResult, okOut, error) {
|
|
if err := CheckToolFromCtx(ctx, "delete_prompt_template"); err != nil {
|
|
return nil, okOut{}, err
|
|
}
|
|
c, err := deps.Caller.Resolve(ctx)
|
|
if err != nil {
|
|
return nil, okOut{}, err
|
|
}
|
|
tid, err := uuid.Parse(in.TemplateID)
|
|
if err != nil {
|
|
return nil, okOut{}, errs.Wrap(err, errs.CodeInvalidInput, "template_id parse")
|
|
}
|
|
if err := deps.Templates.Delete(ctx, c.UserID, c.IsAdmin, tid); err != nil {
|
|
return nil, okOut{}, err
|
|
}
|
|
return nil, okOut{OK: true}, nil
|
|
})
|
|
}
|
|
|
|
// ===== list_models =====
|
|
|
|
type listModelsIn struct {
|
|
IncludeDisabled bool `json:"include_disabled,omitempty" jsonschema:"include disabled models (default false)"`
|
|
}
|
|
type modelSummary struct {
|
|
ID string `json:"id"`
|
|
ModelID string `json:"model_id"`
|
|
DisplayName string `json:"display_name"`
|
|
ContextWindow int `json:"context_window"`
|
|
MaxOutputTokens int `json:"max_output_tokens"`
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
type listModelsOut struct {
|
|
Models []modelSummary `json:"models"`
|
|
}
|
|
|
|
func registerListModels(srv *mcpsdk.Server, deps ServerDeps) {
|
|
mcpsdk.AddTool(srv,
|
|
&mcpsdk.Tool{Name: "list_models", Description: "List available LLM models (for create_conversation)."},
|
|
func(ctx context.Context, _ *mcpsdk.CallToolRequest, in listModelsIn) (*mcpsdk.CallToolResult, listModelsOut, error) {
|
|
if err := CheckToolFromCtx(ctx, "list_models"); err != nil {
|
|
return nil, listModelsOut{}, err
|
|
}
|
|
if _, err := deps.Caller.Resolve(ctx); err != nil {
|
|
return nil, listModelsOut{}, err
|
|
}
|
|
ms, err := deps.Endpoints.ListModels(ctx, !in.IncludeDisabled)
|
|
if err != nil {
|
|
return nil, listModelsOut{}, err
|
|
}
|
|
out := listModelsOut{Models: make([]modelSummary, 0, len(ms))}
|
|
for _, m := range ms {
|
|
out.Models = append(out.Models, modelSummary{
|
|
ID: m.ID.String(), ModelID: m.ModelID, DisplayName: m.DisplayName,
|
|
ContextWindow: m.ContextWindow, MaxOutputTokens: m.MaxOutputTokens,
|
|
Enabled: m.Enabled,
|
|
})
|
|
}
|
|
return nil, out, nil
|
|
})
|
|
}
|