AI
MCP Server
Expose GoLazy application tools, resources, prompts, skills, and MCP Apps UI resources through /mcp.
GoLazy applications can expose an MCP endpoint at /mcp. The endpoint is only
mounted when lazyapp.Config.MCP registers at least one module, so ordinary
apps do not expose MCP by accident.
Use MCP when an AI client should call application tools, read application data, load prompts, discover app-owned skills, or show an interactive MCP Apps UI resource.
Directory shape
Place MCP modules under app/mcps. Use a directory ending in _mcp, and keep
the package name short:
app/mcps/admin_mcp/adminmcp.go
app/views/mcp/admin/dashboard.html.tpl
The module type embeds lazymcp.Base. If Name() returns an empty string,
GoLazy infers the module name from the type, removes the trailing MCP, and
converts it to snake case. AdminMCP becomes admin.
Define a module
package adminmcp
import (
"context"
"errors"
"golazy.dev/lazyauth"
"golazy.dev/lazymcp"
)
type AdminMCP struct {
lazymcp.Base
}
func New(ctx context.Context) *AdminMCP {
return &AdminMCP{Base: lazymcp.NewBase(ctx)}
}
type UserCountParams struct {
Active bool `json:"active"`
}
type UserCountResult struct {
Count int `json:"count"`
}
func (m *AdminMCP) UserCountTool(ctx context.Context) lazymcp.ToolSpec {
return lazymcp.ToolSpec{
Desc: "Count users.",
Fn: m.UserCount,
UI: lazymcp.UI("ui://admin/dashboard"),
}
}
func (m *AdminMCP) UserCount(ctx context.Context, params UserCountParams) (UserCountResult, error) {
user, ok := lazyauth.FromContext(ctx)
if !ok {
return UserCountResult{}, errors.New("sign in required")
}
_ = user
return UserCountResult{Count: 3}, nil
}
The method suffix exports the spec. UserCountTool exports a tool named
user_count unless the spec sets Name. MCP clients see the fully qualified
tool name admin.user_count.
Desc should be set for every model-facing tool, resource, prompt, skill, and
app so clients have useful discovery text.
Register modules
Follow the config-file convention with init/mcp.go:
package appinit
import (
"context"
"example.com/app/app/mcps/admin_mcp"
"golazy.dev/lazymcp"
)
func RegisterMCP(ctx context.Context, scope *lazymcp.Scope) error {
if err := scope.Register(adminmcp.New(ctx)); err != nil {
return err
}
return nil
}
Then wire it into startup:
return lazyapp.New(lazyapp.Config{
Name: "example.com",
Drawer: Draw,
Public: app.Public,
Views: app.Views,
Dependencies: Dependencies,
Auth: Auth,
OAuth: OAuth,
MCP: RegisterMCP,
})
When OAuth is configured, GoLazy protects /mcp with bearer-token
validation. Without OAuth, the MCP endpoint is not authenticated by default.
Do not expose sensitive tools without OAuth or an explicit MCP authorizer.
Export resources and prompts
Resources return data that clients can read:
func (m *AdminMCP) UsersResource(ctx context.Context) lazymcp.ResourceSpec {
return lazymcp.ResourceSpec{
Name: "users",
URI: "app://admin/users",
Desc: "Current user summary.",
MIMEType: "application/json",
Read: func(ctx context.Context) (lazymcp.ResourceContent, error) {
return lazymcp.ResourceContent{
Text: `{"count":3}`,
MIMEType: "application/json",
}, nil
},
}
}
Prompts return messages, or generate them from arguments:
func (m *AdminMCP) AuditPrompt(ctx context.Context) lazymcp.PromptSpec {
return lazymcp.PromptSpec{
Desc: "Ask for an audit summary.",
Messages: lazymcp.UserMessages("Summarize the latest admin audit events."),
}
}
Modules can also implement aggregate methods such as Tools(ctx),
Resources(ctx), Prompts(ctx), Skills(ctx), or Apps(ctx) when the specs
are better built as slices.
Export skills
Skills are served through MCP resources. A module can expose an embedded skill directory:
func (m *AdminMCP) OncallSkill(ctx context.Context) lazymcp.SkillSpec {
return lazymcp.SkillSpec{
Path: "oncall",
FS: oncallSkillFS,
}
}
Clients can read skill://index.json, then fetch
skill://admin/oncall/SKILL.md and supporting files. GoLazy also supports
resources/directory/read for skill directories.
Export MCP Apps
MCP Apps let a server predeclare UI resources with ui:// URIs and link tools
to those resources. The
MCP Apps proposal
standardizes that shape for interactive UI resources in MCP hosts. GoLazy
renders app UI from app/views/mcp/<module>/...:
func (m *AdminMCP) DashboardApp(ctx context.Context) lazymcp.AppSpec {
return lazymcp.AppSpec{
Name: "dashboard",
Desc: "Admin dashboard.",
View: "dashboard",
UseLayout: true,
Permissions: lazymcp.AppPermissions{Tools: []string{"admin.user_count"}},
}
}
The view lives at:
app/views/mcp/admin/dashboard.html.tpl
If UseLayout is true and no layout is set, GoLazy uses mcp_app. You can
also set HTML directly on AppSpec for a static UI resource.
UI-enabled tools should still return useful text or structured content. Not every MCP host supports interactive UI, and text-only clients should still get an answer.
Authorize modules
When a validated JWT is present, the default MCP authorizer reads the mcps
extra claim. Tokens with {"mcps":["admin"]} can access the admin module.
Tokens with {"mcps":["*"]} can access every module. Tokens without that claim
cannot access any module.
Override this with lazyapp.Config.MCPOptions.Authorizer when the app needs
custom policy:
MCPOptions: lazymcp.Options{
Authorizer: func(ctx context.Context, module string) bool {
claims, ok := lazyjwt.ClaimsFromContext(ctx)
return ok && claims.HasScope("mcp:"+module)
},
},
The MCP tool implementation should still enforce operation-level policy before reading or changing application data.