Attaching JSON web services to Buffaly tools
Model an external JSON web service as a ProtoScript service, then expose small typed actions that call named JSON routes through a shared helper.
Core idea
Use this pattern when a product already exposes JSON web service routes and Buffaly needs a typed, discoverable tool surface instead of ad hoc HTTP calls. The service binding owns transport details, the actions own semantic tool names and typed parameters, and JsonWsHelper owns HTTP mechanics and secret resolution.
Keep BaseUrl, API prefixes, token keys, and helper methods on a ProtoScript Service.
Expose one safe remote operation per OpsAction wrapper and pass a service binding parameter.
Store only logical keys such as FeedingFrenzy.ApiKey in the binding; resolve values from UserSecrets at call time.
Reference implementation
The Feeding Frenzy JSONWS integration is the reference pattern. It turns route-based endpoints under https://ff.medek.ai into Buffaly tools while keeping endpoint configuration, authentication, and route execution reusable.
Artifacts
Skills/FeedingFrenzyJsonWs/index.ptsSkills/FeedingFrenzyJsonWs/Leads.ptsSkills/FeedingFrenzyJsonWs/LeadStatuses.pts
Core symbols
FeedingFrenzyJsonWsSkill, FeedingFrenzyJsonWsSkillAction, FeedingFrenzyJsonWsService, FeedingFrenzyJsonWsService#Remote, FeedingFrenzyJsonWsService#Local, and LeadStatuses_GetLeadStatuses.
Overall shape
| Layer | Purpose | Feeding Frenzy example |
|---|---|---|
| Base action | Groups every discoverable tool in the skill. | FeedingFrenzyJsonWsSkillAction |
| Skill entity | Points Buffaly discovery at the action root. | FeedingFrenzyJsonWsSkill |
| Service contract | Centralizes URL, prefixes, token key, initialization, and call helpers. | FeedingFrenzyJsonWsService |
| Actions | Validate inputs, build route arguments, and call the service helper. | LeadStatuses_GetLeadStatuses |
Service-owned binding and helper methods
Actions should not assemble full URLs or read secrets. They pass a route path and a JsonObject to the service. The service passes its BaseUrl, API prefix, and TokenKey to JsonWsHelper.CallJsonRouteSecure(...).
prototype FeedingFrenzyJsonWsService : Service
{
String BaseUrl = new String();
String BusinessApiPrefix = "api/feedingfrenzy.admin.business";
String UiApiPrefix = "api/feedingfrenzy.admin.ui";
String TokenKey = "FeedingFrenzy.ApiKey";
function Initialize() : string
{
if (StringUtil.IsEmpty(this.BaseUrl))
return "Error: BaseUrl must be set on the binding.";
return "initialized";
}
function CallBusinessRoute(string routePath, JsonObject args) : string
{
JsonObject normalizedArgs = args == null ? new JsonObject() : args;
return JsonWsHelper.CallJsonRouteSecure(this.BaseUrl, this.BusinessApiPrefix, routePath, this.TokenKey, normalizedArgs);
}
}
Multiple service instances
Represent environments as partial prototypes of the same service type. Add staging, QA, customer-specific, or regional bindings by changing the instance name, BaseUrl, and TokenKey; do not duplicate every action.
#Remotehttps://ff.medek.ai with FeedingFrenzy.ApiKey.
#Localhttp://feedingfrenzy.local with FeedingFrenzy.Local.ApiKey.
Each action accepts the base service type, for example Execute(FeedingFrenzyJsonWsService service), so the caller chooses the binding at runtime.
Action wrappers
Each wrapper should validate that a service binding was supplied, build a JsonObject with exactly the parameter names the JSONWS route expects, and call either CallBusinessRoute(...) or CallUiRoute(...).
No-argument route
LeadStatuses_GetLeadStatuses checks the binding and calls lead-statuses/get-lead-statuses with an empty argument object.
Parameterized route
Leads_GetLeadsByEmail adds Email to the argument object and calls leads/get-leads-by-email.
Leads_GetPotentialDuplicates must call leads/get-lead first and pass the returned row to another route, document that dependency in the action description.Route naming conventions
The route path should match the published JSONWS route segment, not the C# method name. Keep route strings literal and local to the action unless an authoritative generator owns them.
Authentication and secret handling
A JSONWS binding should store only a logical secret key. The secret value belongs in UserSecrets, not in .pts, .json, .cs, wiki, markdown history, logs, or prompt output.
- Use keys such as
FeedingFrenzy.ApiKeyandFeedingFrenzy.Local.ApiKey. - Import legacy environment variable values into canonical UserSecrets without printing them.
- Smoke tests should report that a key resolves and a route returns data, never the bearer token itself.
Validation checklist
- Compile the ProtoScript project after adding the service and actions.
- Use semantic search or skill listing to confirm the new actions are discoverable.
- Load one action through
ToLoadProtoScriptActionTool. - Call the action with the intended service binding.
- Confirm missing credentials fail fast with a clear logical secret key.
- Configure the canonical user secret without exposing its value.
- Retry and confirm the remote service returns the expected data shape.
- Add focused regression tests when the helper or projection layer changes.
LeadStatuses_GetLeadStatuses, call it with FeedingFrenzyJsonWsService#Remote, resolve FeedingFrenzy.ApiKey through UserSecrets, and confirm rows such as Not Contacted, Contacted, In the Pipeline, Sold, Lost, and Defunct.Scaling the pattern to a larger API
For a small API, a few actions in index.pts may be enough. For a larger API, split actions by remote class or route family and include them from index.pts. This mirrors upstream JSONWS classes and makes route comparison or regeneration easier.
Common mistakes
- Do not put full URLs in every action; put
BaseUrland API prefixes on the service binding. - Do not read UserSecrets in every action; store
TokenKeyon the binding and call the secure helper. - Do not create separate action copies for local and remote; create separate service instances and pass the selected binding.
- Do not silently default a missing service binding; fail fast with a clear error.
- Do not expose secret values while testing.
- Do not mirror destructive or unbounded routes blindly; start with safe read-only routes and add safeguards.