Skip to content

docs(core): add a runnable Parallel Search MCP example - #589

Merged
lIang70 merged 7 commits into
GizClaw:mainfrom
georgeatparallel:parallel-search-5ab58cd4-c0e4-4200-aede-2da7c6e6fd04
Oct 7, 2026
Merged

lIang70 merged 7 commits into
GizClaw:mainfrom
georgeatparallel:parallel-search-5ab58cd4-c0e4-4200-aede-2da7c6e6fd04

Conversation

@georgeatparallel

@georgeatparallel georgeatparallel commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Adds an opt-in standalone example for anonymous web search and URL fetch through FlowCraft's existing HTTP MCP factory, registry and executor. The example lives in examples/parallel, pins released core v0.4.9, and needs no Parallel API key or model provider.

From the repository root:

cd examples/parallel
GOWORK=off go run . -query "FlowCraft Go MCP tool bridge"
GOWORK=off go run . -query "Go language documentation" -fetch https://go.dev/doc/
GOWORK=off go test ./...

It sets a FlowCraft User-Agent, shares a session identifier across search and fetch, and respects cancellation and a total timeout. Text results preserve the server's JSON, URLs and excerpts; empty or non-text-only results return an error. Anonymous access has lower rate limits. Existing deployment defaults are unchanged.

Validation: standalone build, vet and offline tests passed with GOWORK=off against core v0.4.9, including a clean checkout with fresh caches and CLI installation. Fixture tests exercise discovery, registry execution, anonymous headers, ordered search/fetch results, failures, deadlines and cancellation. The prior anonymous endpoint check returned URLs and excerpts for the unchanged request path; this move was validated offline. Workspace CI and release checks passed locally. Workspace and CI module-list additions are left for the maintainers as requested.

For broader search API evaluations, see Artificial Analysis. Those results do not measure this example's anonymous MCP calls.

Btw - I'm a devrel engineer at Parallel

@lIang70

lIang70 commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator

Thanks for this one - it is a compact, self-contained example, and the offline suite (httptest MCP fixture plus a request log) is a good pattern. I reviewed the full diff at f8e640d4 locally: go test ./tool/mcp/... (including -race), go vet, gofmt and golangci-lint all pass, and the fixtures stay hermetic with dead HTTP_PROXY/HTTPS_PROXY set. Nothing below blocks interactive use of the example; most items concern what the README tells a reader to copy and how much of the advertised behavior is actually covered.

Process note: GitHub reports no CI checks on this head, so "make ci passed locally" has no remote evidence on the PR yet.

Paths below for the example are relative to core/tool/mcp/examples/parallel/.

P1

  1. The output contract is asserted only by a test CI can never run - main_test.go:153-185 (gate :158, decode :168-183, fixture text :106). The offline fixture returns a bare prose string, while the JSON-shape assertions (two concatenated documents, results[].url, results[].excerpts) live behind FLOWCRAFT_PARALLEL_LIVE=1. No workflow sets that variable (CI runs go test -count=1 -race ./..., .github/workflows/ci.yml:205), so the multiline-JSON decode added in f8e640d4 is never executed by an automated gate. Also run prints result.Content.Text() unconditionally (main.go:82), so an IsError: false result with empty text prints a blank line and exits 0; the offline tests cannot see that either. Suggest making the fixture return the realistic payload, moving the decoder loop into TestSearchAndFetch, and keeping the live test for endpoint liveness.

P2

  1. The copy-paste spec carries the example's outbound identity and a startup contract - README.md:29, servers.json:7-8. servers.json:7 sets User-Agent: FlowCraft/parallel-mcp-example and :8 sets required: true. Copied verbatim into an application, every request announces itself as the demo string, and required: true makes an anonymous, rate-limited endpoint a startup dependency for any host written to the guidance at docs/guides/tool.md:134-135 (core itself just records the flag, core/tool/mcp/source.go:765-773). The example does not need it: main.go:61 awaits WaitReady unconditionally. Suggest saying next to the copy instruction that both fields must be replaced per deployment (own User-Agent; drop required unless the host must refuse to start without Parallel), or removing them from the embedded spec.

  2. session_id is sent to a tool that never declares it - main.go:73, main_test.go:103-107. The fixture declares an empty object schema and ignores arguments; main_test.go:68-72 only asserts that a >= 32 char session_id is present and constant across calls. Nothing in the repo proves the endpoint accepts the field. If it tightens argument validation, every documented command fails server-side and looks like a bridge bug. Suggest capturing the live tools/list schema into testdata and asserting against it, or dropping the field with a README line that it is endpoint-specific.

  3. The demonstrated executor has no result-size bound, and the README recommends copying the pattern - main.go:65, README.md:29-30. No model context is involved inside this CLI, but the README frames the example as the template to copy into an application, and in this repo a tool result is exactly what rides every later turn's context. The only text bound, result_limit.max, is opt-in: core/tool/middleware/settings.go:88-96 ("Text is unbounded unless the deployment asks for a rune budget"). A search result with excerpts for N sources is remote-controlled and easily crosses 1k tokens. Suggest adding middleware.ResultLimiter(...) in the example and/or one README sentence naming result_limit.max as the host-side control.

  4. web_fetch failure and connect/discovery failure are untested, though the README promises both - main_test.go:105, :142, main.go:62. fixture fails both tools at once and TestToolError passes fetchURL == "", so the fetch error branch never runs; the connect Parallel MCP branch has no coverage. README lines 22-23 advertise "Connection failures and MCP tool errors produce a nonzero exit code". Suggest parametrizing the fixture per tool and adding a connect-failure case with a closed listener plus a short ctx (note WaitReady(ctx, "parallel", 0) does not classify a dial failure as validation, so the test must bound itself).

  5. TestCanceled asserts too little and uses the live spec - main_test.go:148-154. It only checks err != nil, so any unrelated failure passes and the advertised Ctrl-C path is unverified. It also passes the real embedded spec; that is safe today only because AddServer returns early on an already-canceled context (core/tool/mcp/source.go:373-375). If run ever tolerates a pre-canceled context, plain go test would make real network calls, contradicting README.md:38. Suggest using fixture(...), asserting errors.Is(err, context.Canceled), and adding a mid-flight cancellation case against the fixture.

P3

  1. http_timeout: 60s semantics and a test gap - servers.json:9, core/tool/mcp/config.go:222-232, core/tool/mcp/transport.go:87-89, main_test.go:129, :165. The timeout applies only when the client is nil (the production path) and becomes a whole-client timeout, while the package doc explains why a standing SSE session should not have one. Every asserting test injects a client, so the documented default combination is never exercised end to end. Suggest dropping it from the embedded spec (the guide already shows the field at docs/guides/tool.md:126) or adding a nil-client fixture test.

  2. ${env:...} is not expanded on the factory path, and the pointer sits next to a guide that uses it - docs/guides/tool.md:125, :130-131, core/tool/mcp/config.go:123-124, core/deploy/builder.go:319. ParseSpec decodes strictly without expansion; expansion happens in the deploy loader. A reader reusing the page's credential pattern gets the literal Bearer ${env:MCP_TOKEN} on the wire with no error. Suggest one sentence in the README or the guide paragraph.

  3. The new guide link resolves in the repo but escapes the published docs root - docs/guides/tool.md:130. docs/_config.yml publishes docs/ as the Jekyll site root with relative_links: enabled, so the rendered page points at <site>/core/..., which 404s. Same style as the pre-existing docs/guides/inference.md:87, so this is a pattern rather than a new class of bug. Suggest the absolute GitHub URL.

  4. Placement: the first examples/ command inside a released module - main.go:1-2. core/**/examples/** is new here; examples/forge is a separate, unreleased module excluded from lint (.github/workflows/ci.yml:43). This package ships in the core module zip and is lint/test gated with core, so a third-party endpoint demo becomes part of core's released surface. Suggest settling it in this PR: move to a standalone examples/<name> module, or state in CONTRIBUTING/README that in-core examples are non-contractual scaffolding that may move without release notes.

  5. Unchecked type assertion and hard-bound name/prefix - main.go:53, :61, :78. The repo's own pattern is the checked form (core/runtime/dynamic.go:58-60). Also WaitReady(ctx, "parallel", 0) and "parallel__" + name hard-bind the spec; a reader who renames the server or adds prefix (docs/guides/tool.md:118) gets an unknown-tool result and exit 1. Suggest a checked assertion and deriving both from the decoded spec.

  6. The inserted paragraph separates the YAML example from the sentence explaining it, and the skill reference is left behind - docs/guides/tool.md:129-132. The new text sits between the YAML (:127) and "required: true marks..." (:134), which explains the YAML at :120. The sibling copy-paste surface in skills/flowcraft-config/references/resources.md:408-429 was not updated. Suggest moving the pointer below the required/liveness paragraphs and adding the same line there.

  7. The example hand-rolls registry wiring while claiming host parity - main.go:55-65. Hosts go through tool.NewAssembly, which attaches sources (core/tool/assembly.go:83-86; used by core/tool/resource.go:112 and core/tool/middleware/resource.go:74). Suggest tool.NewAssembly([]tool.Source{source}) plus assembly.Execute, dropping Attach/NewExecutor.

  8. "expose the discovered tools in your existing registry" omits the injection budget - README.md:30, core/tool/registry.go:184-194, core/tool/exposure.go:65-78. Registry.Definitions() is uncapped; the bound lives in the dynamic policy (MaxDefinitions: 32, MaxBytes: 16 KiB). MCP discovery is third-party controlled. Suggest one sentence pointing at the policy/budget section.

  9. Content.Text() silently drops non-text parts - main.go:82, core/message/message.go:255, core/tool/mcp/adapter.go:204-221, README.md:18. The bridge maps media to typed parts and a structuredContent-only result to a JSON part, so fetching an image/PDF-ish URL (or a structuredContent-only server) prints an empty line and exits 0, while the README says results are printed. Suggest narrowing the README claim or printing a one-line summary for non-text parts.

  10. Vendor policy claims are asserted in-repo - README.md:33-36. Free tier, rate limits, server-managed fast settings and separate model costs cannot be validated or maintained here and are exactly the claims that rot. Suggest linking Parallel's docs/terms and marking them as quoted.

  11. fixture rewrites only servers[0] through unchecked assertions - main_test.go:116. A shape change panics with an interface-conversion message; worse, a second server entry would silently dial the real endpoint from the offline suite. Suggest building the fixture from mcp.Spec types, or failing loudly when len(servers) != 1.

  12. observedRequest records no destination - main_test.go:20-28, :36-52. Adding Host (and asserting it in check, with len(rows) > 0) is the cheapest guard against accidental real-endpoint calls.

  13. Body capture depends solely on GetBody and degrades silently - main_test.go:38-46. Without GetBody every row records empty values and the failure surfaces as "missing discovery"; a non-JSON auxiliary request instead aborts the real request via return nil, err. Suggest reading/restoring req.Body and tolerating non-JSON.

  14. The output assertion cannot attribute results to tools - main_test.go:106, :132. Both tools return identical text, so the count only proves "two results were printed". Suggest echoing the invoked tool name in the fixture and asserting ordered output (which also de-duplicates the repeated literals).

  15. The CLI surface has no test - main.go:31-43. Flag validation and exit codes 2/1 are only reachable through main(), and the README's "-timeout bounds discovery and calls" has no executed assertion. Suggest extracting the validation into a pure helper tested table-driven; optionally a built-binary smoke test.

  16. Different transports under test; proxy safety is implicit - main_test.go:125 vs :161, core/utils/httpkit.go:211-217. Offline observation wraps http.DefaultTransport while the live and production paths use utils.NewRoundTripper(); both keep ProxyFromEnvironment, and the fixture is hermetic only because httptest serves loopback (verified: still green with dead proxy env). Suggest one explicit hermetic base (for example utils.NewRoundTripper(utils.WithoutRetry())) for the offline tests.

  17. run's client parameter exists only for the tests - main.go:47-48. Production always passes nil, so it is a test-only seam in the implementation shape. Suggest observing at the fixture handler instead; if the seam stays, the doc comment already explains it.

Checked, no findings

  • servers.json matches the strict mcp.ServerSpec schema (core/tool/mcp/config.go:29-41, strict decode, transport and duration both validate). No unknown or mistyped key.
  • Documented commands, tool names (parallel__...), the offline claim (README.md:38) and the exit-code behavior match the code; AddServer rejects a canceled context before scheduling, so go test ./... performs no network I/O.
  • Race safety, t.Cleanup usage, multiline JSON decode correctness and test conventions (stdlib testing, same-package tests, env-gated live lane) are fine.
  • Change size: +338/-0 across 5 files, under the 500-line bar for logic changes, and there is no coherent smaller stage (the tests and docs depend on the new package and on run's parameters).
  • No breaking changes to existing APIs, CLI flags, configuration decoding or checkpoints; nothing behind the example changes behavior for existing deployments.

I could not reach the live endpoint from here, so claims about what search.parallel.ai accepts or returns are reasoned from the code paths above only. Happy to re-review after changes.

@georgeatparallel

Copy link
Copy Markdown
Contributor Author

Thanks for the review. The offline suite now checks distinct, ordered search/fetch JSON documents, URLs, excerpts and end-of-output, including the default client path. Empty or non-text-only results return a clear error. Added fetch failure after successful search, a bounded discovery stall and mid-flight cancellation coverage.

The README now explains deployment User-Agent configuration, host-enforced startup readiness and application result/exposure budgets. I kept session_id: the current Parallel docs support it on both tools. The example still uses the existing factory, registry and executor, and prints text without arbitrary truncation.

Local checks and clean-checkout offline execution passed. The prior anonymous live validation covers the unchanged request path; I did not consume another live request for this correction. GitHub CI still needs its own result.

Btw - I'm a devrel engineer at Parallel

@lIang70

lIang70 commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Thanks for the updates - the behavior items look settled. One structural change is left before this can merge: the example has to move out of core.

core/tool/mcp/examples/parallel is the first example inside a released module - there was no examples/ tree under core/ before this PR - so it ships in every core module zip and is vetted, linted and tested as part of core in CI. Demo code should not ride along in the library. The house arrangement is examples/forge: its own module, excluded from lint (.github/workflows/ci.yml:43), built with GOWORK=off against pinned releases, so it never enters a module zip.

So please move it to examples/parallel as its own module, following the forge pattern:

  • own go.mod (module github.com/GizClaw/flowcraft/examples/parallel), built with GOWORK=off and pinned to a released core. v0.4.9 is current, and every API the example calls (NewFactory, WithHTTPClient, Source.Attach, Source.WaitReady) is already in that tag. I built a copy as a standalone module pinned to v0.4.9: build, vet and the offline suite pass unchanged, so the move does not wait on a new core release.
  • the README run/test blocks become module-local (cd examples/parallel && go run . -query ..., go test ./...).
  • the same commit updates the three references that still carry the old path: the run/test commands in the example README (core/tool/mcp/examples/parallel/README.md:14), docs/guides/tool.md:141, and skills/flowcraft-config/references/resources.md:429.

The bookkeeping entries - go.work, the Makefile module lists, and the CI matrix dirs (.github/workflows/ci.yml:37) - we can take on our side once the move lands. Ping us when it's pushed and we'll re-check.

@georgeatparallel

Copy link
Copy Markdown
Contributor Author

@lIang70 moved the example to examples/parallel with its own module pinned to released core v0.4.9. Updated the README commands and both guide references; the Go code and server settings are unchanged.

GOWORK=off build, vet and offline tests passed, including CLI installation from a clean checkout with fresh caches. Left go.work, the Makefile lists and CI matrix for your side as requested. Ready for another look.

@lIang70

lIang70 commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator

Thanks George — after looking at it, we're keeping examples/parallel exactly as you left it. It's deliberately outside go.work and the CI matrix: the README frames it as the standalone sample of the released artifact (GOWORK=off, core v0.4.9), a different role from examples/forge (the in-workspace demo). Nothing to change on your side — CI is green as it stands. If the merge gate wants the branch current, it's 5 commits behind main.

@lIang70
lIang70 merged commit 6e79bd8 into GizClaw:main Oct 7, 2026
18 of 20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants