Server — rcp-sdk/server

For whoever is exposing their own REST endpoints as tools. Builds a manifest tool entry in code instead of hand-writing the JSON shape.

typescript
import { defineTool } from 'rcp-sdk/server';

defineTool() never touches the network — it returns a plain tool object. Serving it is up to you: collect your tools into an array and return { rcpVersion: '0.1', auth, tools } from whatever route your server already has.

defineTool(options)

typescript
import { z } from 'zod';

const getWeather = defineTool({
  name: 'get_weather',
  description: 'Get current weather information for a city.',
  method: 'GET',
  args: z.object({
    city: z.string().describe('City name, e.g. "Paris"'),
  }),
  url: 'https://internal.example.com/weather',
  queryParams: { city: (t) => t.arg('city') },
  responseMappings: { temperatureC: '@temperatureC', conditions: '@conditions' },
});

Options

OptionTypeNotes
namestringRequired.
descriptionstringRequired. What the model sees when deciding whether to call it.
method'GET'|'POST'|'PUT'|'PATCH'|'DELETE'Required.
argsa z.object({...}) schemaDeclares the model-fillable arguments — each field becomes one params entry.
urlstring | (t) => stringRequired. The callback form gets t, typed against args.
queryParams / headersRecord<string, string | (t) => string>
bodyRecord<string, unknown> | (t) => ...Any string value inside (including nested) may use t.arg(...).
responseMappingsRecord<string, string>{ fieldName: '@json.path' }. A path that doesn’t resolve returns undefined for that field rather than failing the call.

t.arg(name)

Available inside the callback form of url/queryParams/headers/body. References one of args’ own fields and expands to the literal string {{name}} — type-checked against args’ keys, so a typo is a compile error rather than a silently-broken template.

typescript
args: z.object({ query: z.string() }),
url: (t) => `https://api.example.com/search?q=${t.arg('query')}`, // -> "...?q={{query}}"

args → params

zodTool param
z.string() / z.number() / z.boolean()type: 'string'|'number'|'boolean'
.describe('...')description: '...'
.optional()required: false (omitted entirely → required: true)

A type this can’t recognize (a wrapped/refined/union field) falls back to 'string'.

defineTool() has no concept of a resolver-bound param, and no way to mark one as “don’t ask the model for this” — that decision belongs entirely to whoever registers your server as a client. Write a clear description on a param like userId so a client operator knows to intercept it. See Resolvers — hide tenant ID from LLM.

Related