When you need a custom MCP integration, start with APIC . This tutorial builds one operation: look up a public sample note from an approved collection. You own the data and permission rule; APIC generates the protocol, schemas and runtime.
The result is a local stdio-only server. It reads an embedded fixture, opens no network listener and offers no file, shell or account access. This is a deliberately small starting point for understanding the integration boundary. APIC is a separate Apache-2.0 project; Sonny remains commercial software.
Prerequisites and tested versions
| Component | Qualified baseline |
|---|---|
| Platform | Linux amd64, POSIX shell |
| Sonny | Installed 0.12.7, source revision 192e590345c6a4bf1ef8f72b043b318084ea84d6 |
| APIC | v0.26.0, commit 4a23718c169185454eab68622cf232bd00f7f1eb |
| Build tools | Go 1.27.3; C compiler for enabled CGO dependencies; Git |
| Local checks | Python 3.13 and an unzip tool |
Install Sonny through your authorized distribution route . No private Sonny source checkout is needed. APIC was anonymously cloned and built with empty Go caches and the public module proxy/checksum database.
Generation, domain tests and protocol checks need no model account. The final
Sonny turn needs a configured API-backed primary model category and supported
provider authentication
. The qualification used a
local model fixture; it does not establish your provider’s availability or terms.
Get the complete example
Download apic-notes.zip , inspect it and extract it into a directory you control:
unzip apic-notes.zip
cd apic-notes
The archive contains all handwritten inputs and locked Go dependencies:
| File | Purpose |
|---|---|
| notes.json | Complete APIC service definition |
| notes.go | Lookup handler and collection permission check |
| approved-notes.json | Public sample data embedded at build time |
| notes_test.go | Known, missing, malformed and denied lookup tests |
| main.go | Handwritten stdio-only launcher |
| verify.py | Protocol discovery/call and refusal checks |
| notes-recipe.yaml | Sonny Recipe restricted to discovery/execution and this server |
go.mod, go.sum | Pinned dependencies for this generated runtime |
gen/ is intentionally absent: you create it with the pinned generator.
There is no need to copy private session data or put a provider key into this
project. The supplied note is public sample text.
Pin and generate
From the example directory:
git clone --depth 1 --branch v0.26.0 https://gitlab.com/devnw/codepros/oss/apic.git ../apic-v0.26.0
git -C ../apic-v0.26.0 rev-parse HEAD
Check that the revision equals the APIC commit in the baseline table. First exercise the repository’s supplied configuration outside your project tree:
go -C ../apic-v0.26.0 run ./cmd/apic generate \
--config configs/example.json --out "$PWD/../apic-example-output" \
--module example.com/apic-example --with-tests --with-fuzz
This checks the README’s configuration-based entry point. Its HTTP/WebSocket example has deployment-specific authentication/origin warnings; do not launch it as this tutorial’s server. The note definition uses only stdio:
go -C ../apic-v0.26.0 run ./cmd/apic generate \
--config "$PWD/notes.json" --out "$PWD/gen" \
--module example.com/sonny-notes/gen --with-tests --with-fuzz
Expect GENERATE_OK endpoints=1 websockets=0 mcp_tools=1.
The module suffix /gen matches imports beneath this project’s generated
directory; it is not a remote package to install.
APIC v0.26.0 requires an API entry in the definition, so notes.json includes
a health definition. The handwritten launcher calls server.WithStdioOnly();
it never starts that HTTP API or loads TLS material. This is a Go library
option used in our launcher, not an assumed --stdio command-line flag.
The generated gen/mcp.MCPServerInterface names Note_lookup with typed
parameters/results. Our handler implements it through server.WithMCPImpl.
Keep those handwritten files outside gen/ rather than editing generated code.
Understand the permission boundary
The request supplies collection and id. Generated validation checks their
shape and identifier constraints. The handler then compares the collection
with its host-owned grant, approved, before looking up an embedded note.
An argument naming another collection does not grant access to it.
The definition’s authn: public does not authenticate a network caller. This
server is a trusted local child process, with no listener. Its parent controls
which binary starts; the handler limits what that binary can return. A hosted
or multi-user adaptation needs its own authenticated caller and scope policy.
Do not infer that generation supplies that policy automatically.
The launcher sends diagnostics to stderr. Stdout belongs to JSON-RPC; adding a startup banner or printing a note there would corrupt the protocol.
Build and check before connecting
go test ./... -count=1
go build -o ./bin/approved-notes ./cmd/notes
python3 verify.py ./bin/approved-notes
The Go command runs generated tests/fuzz seeds and the domain tests. It does not run a prolonged fuzzing campaign. The Python check exercises initialization, discovery, one successful lookup and four refusals over the actual stdio pipe:
| Request | Expected result |
|---|---|
approved, release-checklist | ID, title and text from the embedded fixture |
approved, missing-note | not_found |
Malformed ID ../private | invalid_argument |
private, release-checklist | permission_denied |
Extra override field | invalid_argument |
Expect five passed lines and a final protocol-check message. The direct
server exposes only note_lookup; Sonny can add its own transport helpers
when it connects. Passing these checks does not qualify a modified handler or
a different dataset automatically.
Declare and approve the binary in Sonny
Create a sidecar definition with your actual absolute binary path:
python3 - <<'PY'
import json
from pathlib import Path
binary = Path("bin/approved-notes").resolve()
Path("mcp.json").write_text(json.dumps({"servers": [{
"name": "approved-notes", "type": "stdio",
"command": str(binary), "enabled": True
}]}, indent=2) + "\n")
PY
sonny mcp approve server approved-notes --command "$PWD/bin/approved-notes" --dry-run
sonny mcp approve server approved-notes --command "$PWD/bin/approved-notes"
sonny mcp list --mcp-config ./mcp.json --connect --format json
Review the dry run before the approval command. Approval writes an operator
allowlist entry and pins the executable’s hash and launch definition. The list
command starts the process and enumerates tools; look for note_lookup.
It does not leave a connection running for the next command.
notes-recipe.yaml places the tool and MCP policies inside the literal pack:
metadata. It permits tool_search and tool_exec, and names only this external
server in pack.mcp.allowed. The model discovers the tool before invoking it.
A successful binary approval does not replace Recipe scoping or trusted policy.
Make the Sonny tool call
The following is an explicit host-executed headless MCP workflow. It is
appropriate here because you reviewed and built a small, read-only local
fixture. --headless-mcp connects allowlisted servers without an isolation realm;
it is not proof of sandboxing and does not override a trusted policy that
forbids that launch. If your deployment requires isolation, use its provisioned
realm workflow instead of weakening that requirement.
sonny recipe validate ./notes-recipe.yaml
sonny run --file ./notes-recipe.yaml --mcp-config ./mcp.json \
--headless-mcp --new --no-trace \
--prompt "Read release-checklist from the approved collection using the approved-notes MCP tool."
Expect the model to discover note_lookup, call it and report this fixture text:
Review the change, run the checks, and record the outcome before publishing.
The installed-binary qualification used one discovery call and one execution
call, both successful, with only tool_search/tool_exec exposed to the model.
The returned note reached the local model fixture before its final reply.
A live model can phrase the answer differently; compare the tool result with
verify.py rather than treating an ungrounded answer as proof of a call.
Your model provider receives the prompt, tool description and returned sample
note. Sonny can persist session data locally even with --no-trace. The server
uses embedded data and makes no account or filesystem request for a lookup.
Review these effects again before substituting non-public notes.
Diagnose failures
| Symptom | Check or recovery |
|---|---|
| Generator rejects an unfamiliar schema field | Use the complete definition for this APIC version; its config is not arbitrary JSON Schema. |
missing api entries | Keep the required health definition; the launcher still serves stdio only. |
missing TLS cert/key paths | Build ./cmd/notes, whose launcher selects stdio only, rather than ./gen/cmd/server. |
| Malformed JSON-RPC or missing replies | Remove stdout banners/debug prints; keep diagnostics on stderr and rerun verify.py. |
| Approval no longer matches | Review the new build and definition, then approve the intended hash; rebuilding changes the pin. |
undiscovered_tool | Discover through tool_search first; do not guess internal invocation names. |
| No external tool appears with a restricted Recipe | Check pack.mcp.allowed under metadata, the integration/allowlist gates and the exact server name. |
| Trusted policy refuses headless host MCP | Use the deployment’s supported isolated configuration; do not disable safeguards to force this sample. |
| Provider authentication or model routing fails | Follow credential diagnostics ; protocol checks can run without a provider. |
Maintain the integration
Record the generator commit, dependency locks and binary hash together. For an update, generate into a fresh directory, compare interfaces/schemas and runtime changes, rerun domain/protocol/denial tests, then review the new Sonny approval. Keep data/authorization logic in your own handler. Review new tools, scopes, transports and data effects before extending the sample.
This tutorial qualifies stdio with Sonny 0.12.7. It does not advertise HTTP or WebSocket as Sonny-compatible transports; those are separate protocols and deployment contracts. Continue with MCP trust , configuration and the qualified catalog .