Phase 0 research resolves all 12 open decisions (interface shape, registration, host API, dynamic toggle, migrations, UI panel integration, config format, testing, boundary enforcement, squash, failure notification, integration test). Phase 1 artifacts: data-model.md (Plugin, Registry, Migration, Host, Status, Backup), contracts/plugin.md (Plugin + HasX interfaces), contracts/host.md (Host struct + plugintest constructor), contracts/rest.md (/api/plugins/*), quickstart.md (end-to-end "hello" plugin in 8 steps). All ten constitution gates pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4.2 KiB
4.2 KiB
Quickstart — Write your first SynapBus plugin
Goal: add a "hello" plugin that exposes an MCP action, a REST route, a Web UI panel, and a tiny migration — in under 20 minutes.
1. Create the package
internal/plugins/hello/
├── plugin.go
├── schema/
│ └── 001_initial.sql
├── ui/
│ └── index.html
└── plugin_test.go
2. Write the plugin
internal/plugins/hello/plugin.go:
package hello
import (
"context"
_ "embed"
"encoding/json"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/synapbus/synapbus/internal/plugin"
)
//go:embed schema/001_initial.sql
var migration001 string
//go:embed ui/index.html
var panelHTML []byte
type HelloPlugin struct{ host plugin.Host }
func New() *HelloPlugin { return &HelloPlugin{} }
func (p *HelloPlugin) Name() string { return "hello" }
func (p *HelloPlugin) Version() string { return "0.1.0" }
func (p *HelloPlugin) Init(ctx context.Context, host plugin.Host) error {
p.host = host
return nil
}
func (p *HelloPlugin) Migrations() []plugin.Migration {
return []plugin.Migration{
{Version: 1, Name: "001_initial", SQL: migration001},
}
}
func (p *HelloPlugin) Actions() []plugin.ActionRegistration {
return []plugin.ActionRegistration{{
Name: "say_hello",
Description: "Return a greeting for the given name.",
InputSchema: json.RawMessage(`{"type":"object","properties":{"name":{"type":"string"}}}`),
RequiredScope: plugin.ScopeRead,
Handler: p.sayHello,
}}
}
func (p *HelloPlugin) sayHello(ctx context.Context, args map[string]any) (any, error) {
name, _ := args["name"].(string)
if name == "" { name = "world" }
return map[string]string{"greeting": "hello, " + name}, nil
}
func (p *HelloPlugin) RegisterRoutes(r chi.Router) {
r.Get("/ping", func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(`{"ok":true}`))
w.Header().Set("Content-Type", "application/json")
})
}
func (p *HelloPlugin) WebPanels() []plugin.PanelManifest {
return []plugin.PanelManifest{{ID: "hello", Title: "Hello", Icon: "hand", Route: "/ui/plugins/hello", Scope: "member"}}
}
func (p *HelloPlugin) PanelHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write(panelHTML)
})
}
3. Write the migration
internal/plugins/hello/schema/001_initial.sql:
CREATE TABLE IF NOT EXISTS plugin_hello_greetings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
4. Write the UI panel
internal/plugins/hello/ui/index.html:
<!doctype html>
<html><head><title>Hello</title></head>
<body><h1>Hello, plugin world</h1></body></html>
5. Register it
cmd/synapbus/plugins.go:
package main
import (
"github.com/synapbus/synapbus/internal/plugin"
"github.com/synapbus/synapbus/internal/plugins/hello"
"github.com/synapbus/synapbus/internal/plugins/wiki"
)
func defaultPlugins() []plugin.Plugin {
return []plugin.Plugin{
wiki.New(),
hello.New(), // ← one line
}
}
6. Write the smoke test
internal/plugins/hello/plugin_test.go:
package hello_test
import (
"testing"
"github.com/synapbus/synapbus/internal/plugin/plugintest"
"github.com/synapbus/synapbus/internal/plugins/hello"
)
func TestHelloPlugin(t *testing.T) {
plugintest.Run(t, hello.New())
}
7. Enable in config
synapbus.yaml:
plugins:
wiki: { enabled: true }
hello: { enabled: true }
8. Run
make build
./synapbus serve --data ./data
curl http://localhost:8080/api/plugins/status | jq— you seehellostatus=started.curl http://localhost:8080/api/plugins/hello/ping— returns{"ok":true}.- Open
http://localhost:8080/ui/plugins/helloin a browser — renders the HTML. - Call
say_hellovia MCP:mcp execute "call('say_hello', {name: 'you'})"→{"greeting":"hello, you"}. - Flip to
enabled: false, send SIGHUP — everything above goes away cleanly.
Total new code: ~60 lines of Go. No core code modified.