---
title: OpenAI Agents SDK
description: Connect an agent built with the OpenAI Agents SDK to Felspar, and gate its acting tools with tool guardrails driven by the released pack.
sidebar:
  order: 6
---

An agent built with OpenAI's Agents SDK (JavaScript) connects to Felspar as a Streamable HTTP MCP server with its own agent key. The pre-action gate plugs in as tool guardrails. The example on this page screens job applicants for a recruiting team.

## Connect

```ts
import { Agent, MCPServerStreamableHttp, run } from "@openai/agents";

const felspar = new MCPServerStreamableHttp({
  name: "felspar",
  url: "https://mcp.felspar.ai/mcp",
  requestInit: { headers: { Authorization: `Bearer ${process.env.FELSPAR_AGENT_KEY}` } },
});
await felspar.connect();

const agent = new Agent({
  name: "Applicant screener",
  instructions: "Run the company's Felspar jobs. If no job is named, pick one with list_jobs. Never decide yourself.",
  mcpServers: [felspar],
});

const result = await run(agent, "Screen application APP-2291 for requisition REQ-311.");
```

In this runtime, tool names are the tools' own names: `list_jobs`, `get_job_requirements`, `get_job_decision` and `check_output`. Put the full Felspar method in the instructions: `npx @felspar/sdk skill <dir>` writes the SDK's skill to `<dir>/skills/felspar/SKILL.md`, and its text works as instructions in any runtime.

## Add the pre-action gate

Here the applicant-tracking system's email tool may send only the output the released pack lists under the decided outcome, only for the application Felspar decided on, and only with text Felspar passed:

```ts
import {
  createActionGate,
  felsparJobDescriber,
  felsparOutputCheckVerifier,
  felsparVerifier,
} from "@felspar/sdk/draft/action-gate";
import {
  felsparDecisionOutputGuardrail,
  felsparToolInputGuardrail,
} from "@felspar/sdk/draft/action-gate/openai-agents";

const felsparApi = { origin: "https://api.felspar.ai", agentKey: process.env.FELSPAR_AGENT_KEY };

const gate = createActionGate({
  job: "screen-candidate",
  decisionTool: "get_job_decision",
  outputCheckTool: "check_output",
  tools: {
    send_candidate_email: {
      // The output this tool writes, by its id in the released pack.
      writes: "candidate-update",
      // The case is the application in this field.
      entity: { field: "applicationId" },
      fields: ["applicationId", "to", "subject", "body"],
      outputCheck: { fields: ["body"] },
    },
  },
  verify: felsparVerifier(felsparApi),
  describe: felsparJobDescriber(felsparApi),
  verifyOutputCheck: felsparOutputCheckVerifier(felsparApi),
});

// Each run's context carries a conversation ID, unique per conversation.
const sessionOf = (runContext) => runContext.context.conversationId;
```

Then attach the two guardrails:

- **`felsparToolInputGuardrail(gate, { sessionOf })`** goes in `toolInputGuardrails` on the MCP server whose tools act (or `inputGuardrails` on a function tool). It runs the gate before a gated call. A denial rejects the call, and the model sees the reason and, when the policy gives one, `Next step:` with the company's own next step.
- **`felsparDecisionOutputGuardrail(gate, { sessionOf })`** goes in `toolOutputGuardrails` on Felspar's server. It records each `get_job_decision` result with the `entity_id` it asked about, and each `check_output` result, and never changes the output.

Pass the conversation ID when you run:

```ts
await run(agent, "Screen application APP-2291 and draft the update to the candidate.", {
  context: { conversationId: "conv-7f3a" },
});
```

## `sessionOf` matters

The gate only uses decisions recorded under the same session ID. `sessionOf(runContext)` must return an ID unique to the conversation, never shared between users or conversations. Otherwise one user's decision could authorize another user's action.

## Notes

- Because tool names here are short, make sure no two servers expose a gated tool with the same name.
- The case field (`applicationId` here) must hold the decision's `entity_id` exactly as sent, trimmed, case included. With `entity`, recipients aren't tied to the case, so limit who a tool can address with `testRecipients` or the tool's own settings.
- The adapters use structural types and add no dependencies. They were type-checked against `@openai/agents-core` 0.20.
- The gate needs `@felspar/sdk` 0.2.0 or later, and 0.2.1 or later for jobs whose case IDs Felspar issues or derives ([identity modes](/concepts/subjects-and-escalations#identity-modes)). Its `draft/` modules may change between minor versions until 1.0.
