Skip to content

Host a compact MCP server

Start with an explicitly authorized registry from the registry tutorial. Import the optional adapter:

import "gitlab.com/phpboyscout/go/mcp/server"

Create one server per hosting lifetime:

host, err := server.New(registry, server.WithIdentity("my-tool", "0.1.0"))
if err != nil {
    return err
}
return host.RunStdio(ctx)

Keep application logs on stderr. A connected client sees three fixed tools: search, inspect and invoke. It can invoke a known operation without first searching.

For an HTTP service, mount host.Handler() on the application's existing mux at an explicitly selected path. Authenticate the request before calling the handler, placing the current principal in the context used by your policies. The handler starts no listener. Attach resources with server.WithResources(resources) when needed; their catalogue uses its own explicit policy.

Register host.Shutdown with the owning controls lifecycle and expose host.Status in health aggregation. Shutdown cancels calls and waits within its budget. Keep a shutdown failure visible until the underlying application work has stopped.

To expose individual native tools, add server.WithMode(server.Direct) at construction. Choose separate server instances/endpoints if you need both views. Direct lists remain caller-authorized; selecting the mode grants no extra access.

Both views support resources. Modern HTTP and stdio clients can use the form/URL interactions from the interaction tutorial. The adapter derives supported modes from each request and passes verified resumed answers back to your handler. Report active work with invocation.Progress(ctx, update); Clients supplying a bounded progress token receive advisory notifications while that call runs.

Legacy stdio clients use an explicit answer loop; June clients receive simpler forms and an application-interface fallback for URL/multiselect requests. Read the server reference before integrating a consumer.

Attach an application shell

For a host with embedded views, load your static HTML and enable Apps explicitly:

host, err := server.New(registry, server.WithApps(server.Apps{
    URI: "ui://my-tool/shell",
    HTML: shellHTML,
    Views: []string{"gallery", "player", "editor"},
    CSP: server.AppCSP{
        ResourceDomains: []string{"https://media.example.com"},
        ConnectDomains: []string{"https://studio.example.com"},
    },
}))
if err != nil {
    return err
}

Supply a valid HTML5 document in shellHTML; keep it public and static. This URI belongs exclusively to the shell, so choose a different URI for application resources. The shell should handle ordinary text results and failures as well as its configured views. Leave permissions and external domains empty unless needed.

An authorized operation can select a view without importing the server or SDK:

presentation, err := mcp.NewPresentation(mcp.PresentationSpec{
    View: "gallery",
    Data: json.RawMessage(`{"project":"demo","revision":"9007199254740993"}`),
})
if err != nil {
    return mcp.Outcome{}, err
}
result, err := mcp.NewResult(
    []mcp.Content{mcp.Text("Candidate previews are available in Studio.")},
    mcp.WithPresentation(presentation),
)
return mcp.Finish(result), err

The UI calls the same call_tool dispatcher for its actions. Keep project/revision, confirmation and idempotency checks in the operations; a view identifier conveys no authority. Publish useful text, structured data and authorized links for clients without Apps. Test launch, actions, refresh and fallback in each supported host before treating the integration as accepted.