Skip to main content

MCP Hub Integration

The MCP Hub allows your integration to expose tools that AI agents can use directly. This enables agents to interact with external services through your integration, creating a powerful extension of their capabilities.

What is MCP Hub?

The MCP (Model Context Protocol) Hub is a centralized way for integrations to expose tools to AI agents. When an integration connects an MCP server, those tools become available to any AI agent using Core. Example flows:
  • Agent uses GitHub integration’s create_issue tool to file a bug
  • Agent uses Slack integration’s send_message tool to notify a team
  • Agent uses Linear integration’s create_task tool to assign work

MCP Configuration

Configure MCP in your integration’s spec.json:

HTTP-based MCP

For services with existing HTTP MCP servers:
{
  "mcp": {
    "type": "http",
    "url": "https://api.githubcopilot.com/mcp/",
    "headers": {
      "Authorization": "Bearer ${config:access_token}",
      "Content-Type": "application/json",
      "X-Custom-Header": "${config:custom_value}"
    }
  }
}
Key points:
  • type: "http" - Specifies HTTP transport
  • url - The MCP server endpoint
  • headers - HTTP headers for authentication and configuration
  • ${config:access_token} - Placeholder replaced with user’s OAuth token at runtime
  • Any ${config:key} placeholder pulls from the user’s integration config

Stdio-based MCP

For command-line MCP servers:
{
  "mcp": {
    "type": "stdio",
    "url": "https://integrations.heysol.ai/slack/mcp/slack-mcp-server",
    "args": [],
    "env": {
      "SLACK_MCP_XOXP_TOKEN": "${config:access_token}",
      "SLACK_MCP_ADD_MESSAGE_TOOL": true,
      "SLACK_WORKSPACE_ID": "${config:team_id}"
    }
  }
}
Key points:
  • type: "stdio" - Specifies stdio transport (command-line)
  • url - Binary/executable URL or path
  • args - Command-line arguments array
  • env - Environment variables passed to the process
  • Placeholders work in env vars too

Configuration Placeholders

The ${config:key} syntax pulls values from the user’s integration configuration:

Setup Phase

When user completes OAuth, you set the config:
// account-create.ts
export async function integrationCreate(data: any) {
  const { oauthResponse } = data;

  return [{
    type: 'account',
    data: {
      accountId: user.id.toString(),
      config: {
        access_token: oauthResponse.access_token,
        refresh_token: oauthResponse.refresh_token,
        team_id: user.team_id,
        custom_setting: 'value',
        // This config is accessible in MCP as ${config:*}
        mcp: {
          tokens: {
            access_token: oauthResponse.access_token
          }
        }
      }
    }
  }];
}

Runtime Phase

Core replaces placeholders when connecting to MCP server:
// Before replacement (spec.json)
{
  "mcp": {
    "type": "http",
    "url": "https://api.service.com/mcp/",
    "headers": {
      "Authorization": "Bearer ${config:access_token}",
      "X-Team-ID": "${config:team_id}"
    }
  }
}

// After replacement (runtime)
{
  "mcp": {
    "type": "http",
    "url": "https://api.service.com/mcp/",
    "headers": {
      "Authorization": "Bearer xoxp-actual-token-here",
      "X-Team-ID": "T12345"
    }
  }
}

HTTP MCP Server Example (GitHub)

spec.json

{
  "name": "GitHub extension",
  "key": "github",
  "description": "Manage GitHub repositories and issues",
  "icon": "github",

  "auth": {
    "OAuth2": {
      "token_url": "https://github.com/login/oauth/access_token",
      "authorization_url": "https://github.com/login/oauth/authorize",
      "scopes": ["repo", "user"]
    }
  },

  "mcp": {
    "type": "http",
    "url": "https://api.githubcopilot.com/mcp/",
    "headers": {
      "Authorization": "Bearer ${config:access_token}",
      "Content-Type": "application/json"
    }
  }
}

Available Tools

When Core connects to GitHub’s MCP server, these tools become available:
  • github_create_issue - Create a new issue
  • github_search_code - Search code across repositories
  • github_create_pull_request - Create a new PR
  • github_list_commits - List commits for a repository
Agents can now call these tools:
// Agent calls this internally
await mcpClient.callTool('github_create_issue', {
  owner: 'user',
  repo: 'project',
  title: 'Bug: Login fails',
  body: 'Steps to reproduce...'
});

Stdio MCP Server Example (Slack)

spec.json

{
  "name": "Slack extension",
  "key": "slack",
  "description": "Connect your workspace to Slack",
  "icon": "slack",

  "auth": {
    "OAuth2": {
      "token_url": "https://slack.com/api/oauth.v2.access",
      "authorization_url": "https://slack.com/oauth/v2/authorize",
      "scopes": ["chat:write", "channels:read", "users:read"]
    }
  },

  "mcp": {
    "type": "stdio",
    "url": "https://integrations.heysol.ai/slack/mcp/slack-mcp-server",
    "args": [],
    "env": {
      "SLACK_MCP_XOXP_TOKEN": "${config:access_token}",
      "SLACK_MCP_ADD_MESSAGE_TOOL": true
    }
  }
}

Available Tools

The Slack MCP server exposes:
  • slack_send_message - Send message to channel
  • slack_list_channels - List workspace channels
  • slack_add_reaction - React to a message
  • slack_get_users - Get workspace users

Building Your Own MCP Server

If your service doesn’t have an existing MCP server, you can build one:

1. Use the MCP SDK

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';

const server = new Server(
  {
    name: 'my-service-mcp',
    version: '1.0.0',
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

// Define your tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: 'create_task',
        description: 'Create a new task',
        inputSchema: {
          type: 'object',
          properties: {
            title: { type: 'string' },
            description: { type: 'string' },
          },
          required: ['title'],
        },
      },
    ],
  };
});

// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  if (name === 'create_task') {
    const result = await createTask(args.title, args.description);
    return {
      content: [{ type: 'text', text: `Created task: ${result.id}` }],
    };
  }

  throw new Error(`Unknown tool: ${name}`);
});

// Start server
const transport = new StdioServerTransport();
await server.connect(transport);

2. Authentication

Get tokens from environment variables:
const accessToken = process.env.MY_SERVICE_TOKEN;

if (!accessToken) {
  throw new Error('MY_SERVICE_TOKEN not provided');
}

// Use token in API calls
const response = await fetch('https://api.myservice.com/tasks', {
  headers: {
    Authorization: `Bearer ${accessToken}`
  }
});

3. Expose in spec.json

{
  "mcp": {
    "type": "stdio",
    "url": "/path/to/your-mcp-server",
    "args": [],
    "env": {
      "MY_SERVICE_TOKEN": "${config:access_token}"
    }
  }
}

MCP Tool Best Practices

1. Clear Tool Descriptions

{
  "name": "create_issue",
  "description": "Create a new GitHub issue in a repository. Returns the issue number and URL.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "owner": {
        "type": "string",
        "description": "Repository owner (username or organization)"
      },
      "repo": {
        "type": "string",
        "description": "Repository name"
      },
      "title": {
        "type": "string",
        "description": "Issue title (required, max 256 characters)"
      },
      "body": {
        "type": "string",
        "description": "Issue description in markdown format"
      }
    },
    "required": ["owner", "repo", "title"]
  }
}

2. Return Structured Data

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const result = await createIssue(args);

  return {
    content: [
      {
        type: 'text',
        text: `Created issue #${result.number}: ${result.html_url}`
      },
      {
        type: 'resource',
        resource: {
          uri: result.html_url,
          mimeType: 'text/html',
          text: JSON.stringify(result)
        }
      }
    ],
  };
});

3. Error Handling

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  try {
    const result = await performAction(args);
    return {
      content: [{ type: 'text', text: `Success: ${result}` }],
    };
  } catch (error) {
    return {
      content: [{
        type: 'text',
        text: `Error: ${error.message}`
      }],
      isError: true,
    };
  }
});

4. Rate Limiting

let lastCallTime = 0;
const MIN_INTERVAL = 1000; // 1 second between calls

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const now = Date.now();
  if (now - lastCallTime < MIN_INTERVAL) {
    await new Promise(resolve =>
      setTimeout(resolve, MIN_INTERVAL - (now - lastCallTime))
    );
  }
  lastCallTime = Date.now();

  // Proceed with tool call
});

Testing MCP Tools

1. Test with MCP Inspector

npx @modelcontextprotocol/inspector node your-mcp-server.js

2. Test with Claude Desktop

Add to Claude Desktop config:
{
  "mcpServers": {
    "your-service": {
      "command": "node",
      "args": ["/path/to/your-mcp-server.js"],
      "env": {
        "MY_SERVICE_TOKEN": "test-token"
      }
    }
  }
}

3. Test Tool Calls

// In your integration tests
const client = new Client({
  name: 'test-client',
  version: '1.0.0'
}, {
  capabilities: {
    tools: {}
  }
});

await client.connect(transport);

const tools = await client.listTools();
console.log('Available tools:', tools);

const result = await client.callTool('create_task', {
  title: 'Test task'
});
console.log('Tool result:', result);

Common Patterns

Dynamic Tool Listing

Generate tools based on user’s account:
server.setRequestHandler(ListToolsRequestSchema, async () => {
  const user = await getUserFromToken(accessToken);
  const projects = await getProjects(user.id);

  // Generate tool for each project
  const tools = projects.map(project => ({
    name: `create_task_${project.id}`,
    description: `Create task in ${project.name}`,
    inputSchema: {
      type: 'object',
      properties: {
        title: { type: 'string' }
      }
    }
  }));

  return { tools };
});

Composite Actions

Combine multiple API calls into one tool:
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === 'create_issue_with_assignee') {
    // 1. Create issue
    const issue = await createIssue(args.title, args.body);

    // 2. Assign user
    await assignIssue(issue.number, args.assignee);

    // 3. Add labels
    await addLabels(issue.number, args.labels);

    return {
      content: [{
        type: 'text',
        text: `Created and configured issue #${issue.number}`
      }],
    };
  }
});

Security Considerations

  1. Token Scoping - Only request OAuth scopes needed for MCP tools
  2. Input Validation - Validate all tool parameters before use
  3. Rate Limiting - Implement rate limits to prevent abuse
  4. Error Messages - Don’t expose sensitive data in error messages
  5. Token Storage - Tokens are securely encrypted in Core’s database

Next Steps