Skip to content
Source

MCP servers and secrets

Your custom agent needs data from another service. For example, it links each new bug report to the Linear ticket for it, through Linear's MCP server, with your Linear key.

Give the agent the key as a secret, and the service's host as egress. The sandbox gets a placeholder, not the key. The forge puts the real key only in the request headers to the hosts of the secret.

You need admin access to the repository. Start with a working agent: see Custom agents.

1. Add the key as a CI secret

A custom agent uses the CI secrets of the repository. Bind the secret to the host of the service:

src ci secret set LINEAR_KEY --trusted - --host mcp.linear.app < linear-key.txt

The agent gets the trusted value on issues and pull requests of people with write access. On the items of other people, it gets the untrusted value, or no secret when that level has no value. See Trust levels and secrets.

2. Name the secret and the host in the agent

Add secrets and egress to the agent's forge fields, and connect the MCP server in run:

import { defineFactory } from '@tanstack-source/ci'
import { forgeTools } from '@tanstack-source/ci/agent'
import { chat, defineAgent } from '@tanstack/ai'
import { createMCPClient } from '@tanstack/ai-mcp'

export default defineFactory('house', {
  agents: {
    'link-tickets': {
      agent: defineAgent({
        name: 'link-tickets',
        description: 'Links each new bug report to its Linear ticket.',
        run: async (ctx) => {
          // A placeholder: the forge swaps it for the key in requests to mcp.linear.app only.
          const linear = await createMCPClient({
            transport: { type: 'http', url: 'https://mcp.linear.app/mcp', headers: { authorization: `Bearer ${process.env.LINEAR_KEY}` } },
            prefix: 'linear',
          })
          return chat({
            adapter: ctx.forge!.model(),
            systemPrompts: ['Find the Linear ticket of this bug report. Post one comment with its link.'],
            tools: forgeTools(ctx, ['comment', 'record_verdict']),
            mcp: { clients: [linear] },
            messages: ctx.messages,
            ...ctx.forward,
          })
        },
      }),
      on: ['issue.opened'],
      trigger: { on: 'everyone' },
      budget: { maxTokens: 100_000 },
      secrets: ['LINEAR_KEY'],
      egress: ['mcp.linear.app'],
    },
  },
})

The sandbox has each secret as an environment variable with the same name. Its value is a placeholder that starts with tss_ph_.

3. Save the file and see it run

Save the file as in Save the file in the vault. Then open a new issue: its Factory panel shows the run of link-tickets, and the issue gets a comment with the link.

What the forge allows

  • A request with a placeholder goes out only in a header, and only to a host of that secret. A placeholder in a URL, or to another host, is refused, and the job's error names the secret.
  • The agent reaches only the hosts in egress, on port 443.

Done

Your agent reads Linear through its MCP server, and the key never enters the sandbox.