- Identify which
packages/server/src/tools/file it belongs in (or create a new one) - Import
{ getAuthenticatedRh, text }from./_helpers.js - Register with
server.tool(name, description, zodSchema, handler) - Define the input schema with Zod — MCP uses these for the tool schema
- Wrap the handler body in try/catch, return
text({ error: String(e) })on failure - If a new file, import and call its
register*Tools(server)inserver.ts - Add tests in
packages/server/__tests__/tools.test.ts
Example:
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { getAuthenticatedRh, text } from "./_helpers.js";
export function registerNewTools(server: McpServer): void {
server.tool(
"robinhood_new_tool",
"Tool description shown to agents.",
{
param: z.string().describe("Parameter description."),
},
async ({ param }) => {
try {
const rh = await getAuthenticatedRh();
const result = await rh.someMethod(param);
return text({ data: result });
} catch (e) {
return text({ error: String(e) });
}
},
);
}- Create
.claude/skills/robinhood-<name>/SKILL.mdwith:- YAML frontmatter (
name,description) - Trigger phrases
- Step-by-step instructions for Claude
- Code patterns to follow
- YAML frontmatter (
- Create
.claude/skills/robinhood-<name>/reference.mdwith API details - Keep SKILL.md under 500 lines
- Define a Zod schema in
packages/client/src/types.ts(use.passthrough()) - Add a URL builder in
packages/client/src/urls.tsif needed - Implement the method in
packages/client/src/client.ts:- Use
parseOne(Schema, data)orparseArray(Schema, data)for return values - Use typed return signatures (e.g.
Promise<Quote[]>, notPromise<unknown[]>)
- Use
- Export the new type from
packages/client/src/index.ts - Add tests in
packages/client/__tests__/usingvi.mock("../src/http.js")
npx vitest run # All tests
npx vitest run --watch # Watch modeAll tests mock the HTTP layer via vi.mock() — no real API calls. Use vitest (not bun test) for correct module isolation.
When mocking http.js, use importOriginal to preserve parseOne/parseArray:
vi.mock("../src/http.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../src/http.js")>();
return {
...actual,
requestGet: vi.fn(),
requestPost: vi.fn(),
};
});- Fork the repo and create a branch from
main - Branch naming:
feat/description,fix/description, ordocs/description - Make your changes and ensure all checks pass locally:
bun run check && bun run typecheck && npx vitest run
- Write clear commit messages:
feat: add new tool,fix: handle null margin,docs: update README - Open a pull request against
main - Fill out the PR template (safety checklist + testing)
Before adding any new tool or skill:
- Does it expose fund transfer or bank operations? (If yes, BLOCK it)
- Does it place orders? (If yes, require explicit parameters, add to high-risk tier)
- Could it cause bulk operations? (If yes, consider blocking or adding safeguards)
- Update ACCESS_CONTROLS.md with the new operation