Install
$ agentstack add mcp-goadesign-goa-ai ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Design-First Agentic Systems in Go
Declare agents, tools, MCP servers, policies, and structured model output in Goa. Generate the plumbing. Run it durably.
Quick Start | What You Can Build | How It Works | Production | Learn More
Why Goa-AI
Most agent frameworks start with code and ask you to keep the contracts in your head: JSON schemas, tool names, retry behavior, model output formats, UI events, and workflow state. Goa-AI starts with the contract.
You describe the agent system in the same design-first style as Goa services. goa gen turns that design into typed Go packages: tool specs, JSON schemas, codecs, workflow registrations, clients, MCP adapters, registry clients, and structured completion helpers. The runtime then executes the generated contracts with policy enforcement, streaming, replayable run logs, and an engine you can swap from in-memory development to Temporal-backed production.
| If you care about... | Goa-AI gives you... | | --- | --- | | Strong tool contracts | Goa types, validations, examples, generated JSON Schema, generated codecs | | Durable agent execution | A plan/execute/resume workflow loop with retries, budgets, cancellation, and Temporal support | | Existing service logic | BindTo and generated transforms that connect tools to Goa service methods | | Structured final answers | Service-owned Completion(...) contracts with unary and streaming helpers | | Multi-agent systems | First-class agent-as-tool composition with child runs and linked streams | | Human approval | Await/clarification flows plus design-time and runtime tool confirmation | | Real-time UI | Typed stream events for tool progress, assistant text, usage, awaits, workflow status, and child links | | External tools | MCP callers, generated MCP servers, external MCP schemas, and registry-backed discovery with provider-instance health | | Production operations | Mongo-backed stores, Pulse streaming, OpenAI/Bedrock/Anthropic/gateway clients, telemetry hooks |
Goa-AI is not a prompt wrapper. It is a contract and runtime layer for agentic Go services.
Quick Start
This path gives you a generated, runnable agent and a typed direct-completion helper. The generated example uses the in-memory engine, so there are no external services required.
1. Create a Module
go install goa.design/goa/v3/cmd/goa@latest
mkdir quickstart && cd quickstart
go mod init example.com/quickstart
go get goa.design/goa/v3@latest goa.design/goa-ai@latest
mkdir design
2. Add design/design.go
package design
import (
. "goa.design/goa/v3/dsl"
. "goa.design/goa-ai/dsl"
)
var _ = API("orchestrator", func() {})
var AskPayload = Type("AskPayload", func() {
Attribute("question", String, "User question to answer")
Example(map[string]any{"question": "What is the capital of Japan?"})
Required("question")
})
var Answer = Type("Answer", func() {
Attribute("text", String, "Answer text")
Example(map[string]any{"text": "Tokyo is the capital of Japan."})
Required("text")
})
var TaskDraft = Type("TaskDraft", func() {
Attribute("name", String, "Task name")
Attribute("goal", String, "Outcome-style goal")
Required("name", "goal")
})
var _ = Service("orchestrator", func() {
Completion("draft_task", "Produce a task draft directly", func() {
Return(TaskDraft)
})
Agent("chat", "Friendly Q&A assistant", func() {
Use("helpers", func() {
Tool("answer", "Answer a simple question", func() {
Args(AskPayload)
Return(Answer)
})
})
RunPolicy(func() {
DefaultCaps(MaxToolCalls(2), MaxConsecutiveFailedToolCalls(1))
TimeBudget("15s")
})
})
})
3. Generate and Run
goa gen example.com/quickstart/design
goa example example.com/quickstart/design
go run ./cmd/orchestrator
Expected shape:
RunID: orchestrator-chat-...
Assistant: Hello from example planner.
Completion draft_task: ...
Completion stream draft_task: ...
Generation creates application-owned scaffolding under internal/agents/ and generated contract code under gen/. Edit the planner and bootstrap files; do not edit gen/.
4. Run an Agent from Application Code
The generated agent package exposes a typed client. Sessionful runs require an explicit session; one-shot runs do not.
rt, cleanup, err := bootstrap.New(ctx)
if err != nil {
log.Fatal(err)
}
defer cleanup()
if _, err := rt.CreateSession(ctx, "session-1"); err != nil {
log.Fatal(err)
}
client := chat.NewClient(rt)
out, err := client.Run(ctx, "session-1", []*model.Message{{
Role: model.ConversationRoleUser,
Parts: []model.Part{model.TextPart{Text: "Hello"}},
}})
if err != nil {
log.Fatal(err)
}
fmt.Println(out.RunID)
// For request/response work that should not belong to a session:
out, err = client.OneShotRun(ctx, []*model.Message{{
Role: model.ConversationRoleUser,
Parts: []model.Part{model.TextPart{Text: "Summarize this file"}},
}})
5. Replace the Stub Planner
Planners decide what happens next: final response, tool calls, await human input, or terminal tool result. During runtime-forced finalization, planners may also close through terminal bookkeeping tools; the runtime executes only Bookkeeping() + TerminalRun() tools in that path and requires the terminal side effects to succeed inside the remaining hard-deadline window. Retry-owned tool restrictions do not block this validated terminal path, while caller WithRestrictToTool policy still applies. Tool executors decide how work is performed.
func (p *Planner) PlanStart(ctx context.Context, in *planner.PlanInput) (*planner.PlanResult, error) {
mc, ok := in.Agent.PlannerModelClient("default")
if !ok {
return nil, errors.New("model client default is not registered")
}
summary, err := mc.Stream(ctx, &model.Request{
Messages: in.Messages,
Tools: in.Agent.AdvertisedToolDefinitions(),
Stream: true,
})
if err != nil {
return nil, err
}
if len(summary.ToolCalls) > 0 {
return &planner.PlanResult{ToolCalls: summary.ToolCalls}, nil
}
return &planner.PlanResult{
FinalResponse: &planner.FinalResponse{
Message: &model.Message{
Role: model.ConversationRoleAssistant,
Parts: []model.Part{model.TextPart{Text: summary.Text}},
},
},
Streamed: true,
}, nil
}
Register model clients during bootstrap with rt.RegisterModel(...) or runtime factories such as rt.NewOpenAIModelClient(...), rt.NewBedrockModelClient(...), rt.NewVertexGeminiModelClient(...), and rt.NewVertexAnthropicModelClient(...).
How It Works
design/*.go
Agents, toolsets, completions, policies, MCP, registries
|
| goa gen
v
gen/
Agent packages, tool specs, codecs, schemas, workflow registrations,
typed clients, completion helpers, MCP adapters, registry clients
|
| runtime.New(...)
v
Runtime
Plan -> execute tools -> resume -> finish
Policy, memory, streaming, run log, telemetry, engine integration
|
+-- in-memory engine for development
+-- Temporal engine for durable production workers
The key separation is deliberate:
- The DSL owns contracts: names, schemas, validations, examples, tags, policies, confirmation, MCP exposure, and registry sources.
- Generated code owns repetitive infrastructure: JSON codecs, JSON Schema, route metadata, workflow/activity registrations, client helpers, completion helpers, and transforms.
- The runtime owns execution: planner calls, tool admission, policy checks, tool activities, child workflows, awaits, streaming, memory, run logs, and telemetry.
- Your code owns judgment and side effects: planners, service methods, tool executors, model choice, storage, deployment, UI, and product policy.
What You Can Build
Typed Tools and Toolsets
Toolsets are callable capabilities. They can be inline, service-backed, MCP-backed, registry-backed, or implemented by another agent.
var Docs = Toolset("docs", func() {
Description("Document retrieval tools")
Tags("docs", "read")
Tool("search", "Search indexed documents", func() {
Args(func() {
Attribute("query", String, "Search phrase", func() {
MinLength(1)
MaxLength(500)
})
Attribute("limit", Int, "Maximum results", func() {
Minimum(1)
Maximum(50)
Default(10)
})
Required("query")
})
Return(ArrayOf(Document))
BoundedResult()
CallHintTemplate("Searching docs for {{ .Query }}")
})
})
What you get:
- JSON Schema for LLM function calling (auto-generated)
- Validation at boundaries: invalid calls get structured retry hints, including
generated JSON type mismatch guidance, not crashes or schema-string parsing
- Type-safe Go structs for payloads and results
- Provider-facing examples only when you author a top-level Goa
Example(...)
on the tool payload. Codegen precomputes the annotated schema, the schema with the root example removed, and the parsed example input so OpenAI-style providers consume schema annotations while Anthropic and Bedrock Claude receive provider-native input_examples.
- Explicit control-plane contracts:
Bookkeeping()keeps tools durable,
budget-exempt, and hidden from future planner turns
Bind Tools to Goa Services
Use BindTo when the best tool implementation is already a service method. Use Inject for infrastructure fields that should not be model-visible.
Method("search_documents", func() {
Payload(func() {
Attribute("query", String, "Search phrase")
Attribute("session_id", String, "Current session")
Required("query", "session_id")
})
Result(ArrayOf(Document))
})
Agent("chat", "Document assistant", func() {
Use("docs", func() {
Tool("search", "Search documents", func() {
Args(func() {
Attribute("query", String, "Search phrase")
Required("query")
})
Return(ArrayOf(Document))
BindTo("search_documents")
Inject("session_id")
})
})
})
The generator emits typed transforms where shapes are compatible. Inject names that match a runtime.ToolCallMeta field (run_id, session_id, turn_id, tool_call_id, parent_tool_call_id) are meta-backed; any other name is label-backed, read from labels supplied via runtime.WithLabels(...) at run start. See [docs/dsl.md](docs/dsl.md) and [docs/runtime.md](docs/runtime.md) for the full contract.
Structured Direct Completions
Use Completion(...) when the model should return a typed value directly instead of calling a tool.
var Draft = Type("Draft", func() {
Attribute("name", String, "Task name")
Attribute("goal", String, "Outcome-style goal")
Required("name", "goal")
})
var _ = Service("tasks", func() {
Completion("draft_from_transcript", "Produce a task draft directly", func() {
Return(Draft)
})
})
goa gen emits gen//completions/ with schemas, codecs, completion.Spec values, Complete(...), StreamComplete(...), and DecodeChunk(...). Completion names are part of the contract: 1-64 ASCII characters, letters/digits/_/-, starting with a letter or digit.
Unary helpers request provider-enforced structured output and decode with generated codecs. Streaming helpers expose preview completion_delta chunks but decode only the final canonical completion chunk. Providers that cannot preserve the structured-output contract fail explicitly with model.ErrStructuredOutputUnsupported.
Agent-as-Tool Composition
Agents can export toolsets that other agents use. The nested agent runs as a child workflow, not as flattened helper code.
Agent("researcher", "Research specialist", func() {
Export("research", func() {
Tool("deep_search", "Perform deep research", func() {
Args(ResearchRequest)
Return(ResearchReport)
})
})
})
Agent("coordinator", "Delegates specialist work", func() {
Use(AgentToolset("orchestrator", "researcher", "research"))
})
Parent runs receive a tool result with a child run link. Streams emit child_run_linked so UIs can render nested runs without losing identity, logs, or telemetry.
Runtime Policies, Tags, and Timing
Policies are runtime-enforced, not planner suggestions.
Agent("operator", "Production operations agent", func() {
RunPolicy(func() {
DefaultCaps(MaxToolCalls(20), MaxConsecutiveFailedToolCalls(3))
Timing(func() {
Budget("5m")
Plan("45s")
Tools("90s")
})
InterruptsAllowed(true)
OnMissingFields("await_clarification")
History(func() {
KeepRecentTurns(20)
})
Cache(func() {
AfterSystem()
AfterTools()
})
})
})
History can also use model-assisted compression: declare CompressAtMaxInputTokens or CompressAtTurns triggers plus KeepMaxInputTokens or KeepMaxTurns exact-retention budgets inside History. Token budgets are counted at runtime by a history model that implements model.TokenCounter with exact counts and keep only whole recent turns, never truncated tool exchanges.
Per-run options can further restrict execution:
out, err := client.Run(ctx, "session-1", messages,
runtime.WithRunTimeBudget(2*time.Minute),
runtime.WithRestrictToTool("docs.search"),
runtime.WithTagPolicyClauses([]runtime.TagPolicyClause{
{AllowedAny: []string{"read", "safe"}},
{DeniedAny: []string{"destructive"}},
}),
)
Human-in-the-Loop and Confirmation
Planners can pause for clarification or external tool results. Sensitive tools can require approval before execution.
Tool("change_setpoint", "Change a device setpoint", func() {
Args(ChangeSetpointRequest)
Return(ChangeSetpointResult)
Confirmation(func() {
Title("Confirm setpoint change")
PromptTemplate("Set {{ .DeviceID }} to {{ .Value }}?")
DeniedResultTemplate(`{"status":"denied"}`)
})
})
At runtime, the workflow emits an await-confirmation event, waits for ProvideConfirmation, records a durable authorization event, and only then executes the tool. Denials produce schema-compliant tool results so planners and transcripts remain deterministic.
Bounded Results and Server Data
Large results need two views: a small model-facing view and rich server-side data for UIs or downstream systems.
Tool("get_time_series", "Get a bounded time-series view", func() {
Args(TimeSeriesRequest)
Return(TimeSeriesSummary)
BoundedResult(func() {
Cursor("cursor")
NextCursor("next_cursor")
})
ServerData("charts.points", TimeSeriesPoints, func() {
Description("Chart points for observer-facing UI")
AudienceInternal()
FromMethodResultField("ChartPoints")
})
ServerDataDefault("off")
})
BoundedResult makes truncation explicit through runtime-owned bounds metadata (returned, truncated, optional total, next_cursor, and refinement_hint). Generated tool specs and result JSON use model-facing JSON names, so lower-camel Goa fields such as nextCursor are exposed as next_cursor. Truncated results must carry a continuation: bound method results must define refinement_hint (snake_case, optional String) unless paging is configured, and the runtime rejects truncated results that provide neither a next cursor nor a refinement hint. ServerData attaches rich data that is never sent to model providers.
Bookkeeping and Terminal Tools
Use Bookkeeping() for control-plane side effects such as status updates, progress snapshots, or terminal commits.
Tool("set_step_status", "Update task step status", func() {
Args(SetStepStatusRequest)
Return(TaskProgressSnapshot)
Bookkeeping()
})
Tool("commit_report", "Commit final report", func() {
Args(CommitReportRequest)
Return(CommitReportResult)
Bookkeeping()
TerminalRun()
})
Bookkeeping tools do not consume the normal MaxToolCalls budget. Their events are still durable and streamed. Successful results stay hidden from future planner turns; retryable failures stay visible with their retry hints so the planner can repair the failed call.
During runtime-forced finalization, retry-owned tool restrictions from previous repair attem
…
Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: goadesign
- Source: goadesign/goa-ai
- License: MIT
- Homepage: https://goa.design/docs/2-goa-ai/
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.