# MCP integrations

`tnsai-mcp` ships small clients for a few external services. The Composio client calls Composio's REST API. The database and Stripe clients wrap an `McpClient` that is already connected to a server and call that server's tools by fixed names.

## Example

<!-- java-contract: src/main/java/com/example/tnsai/docs/McpIntegrationsExample.java -->
```java
package com.example.tnsai.docs;

import com.fasterxml.jackson.databind.JsonNode;
import com.tnsai.mcp.client.McpClient;
import com.tnsai.mcp.client.StdioTransport;
import com.tnsai.mcp.composio.ComposioAuthBridge;
import com.tnsai.mcp.composio.ComposioClient;
import com.tnsai.mcp.composio.ComposioDiscovery;
import com.tnsai.mcp.database.PostgresMcpClient;
import com.tnsai.mcp.model.McpToolDefinition;
import com.tnsai.mcp.stripe.StripeMcpClient;
import com.tnsai.mcp.stripe.StripePaymentTool;
import java.io.IOException;
import java.util.List;
import java.util.Map;

/** Compilation contract for mcp/integrations.md. */
public final class McpIntegrationsExample {

    private McpIntegrationsExample() {
    }

    static JsonNode composio(String apiKey, String action, Map<String, Object> input) throws IOException {
        if (!new ComposioAuthBridge(apiKey).verifyApiKey()) {          // false on HTTP 401 or 403
            throw new IOException("Composio rejected the API key");
        }
        ComposioClient composio = new ComposioClient(apiKey);
        if (!composio.getConnectionStatus("github").connected()) {
            throw new IOException("Connect a GitHub account in Composio first");
        }
        List<McpToolDefinition> tools = new ComposioDiscovery(composio).discoverTools("github");
        tools.forEach(t -> System.out.println(t.getName() + ": " + t.getDescription()));
        return composio.executeTool(action, input);                    // an action name from the list
    }

    static JsonNode postgres(String... serverCommand) throws IOException {
        McpClient client = McpClient.create(StdioTransport.builder()
            .command(serverCommand)                                    // a server with an "execute_sql" tool
            .build());
        client.connect();
        client.initialize();
        try {
            PostgresMcpClient postgres = new PostgresMcpClient(client);
            postgres.listTables();
            return postgres.executeQuery("SELECT id, title FROM docs LIMIT 10");   // calls the "execute_sql" tool
        } finally {
            client.disconnect();
        }
    }

    static JsonNode stripe(McpClient connectedClient) throws IOException {
        StripePaymentTool payments = new StripePaymentTool(new StripeMcpClient(connectedClient));
        return payments.execute("create_payment_intent", Map.of("amount", 2_000, "currency", "usd"));
    }
}
```

## Composio

`com.tnsai.mcp.composio` talks to Composio's REST API at `https://backend.composio.dev/api/v2`, not over MCP. Every request sends the API key in the `X-API-Key` header. The constructors reject a null or blank key with `IllegalArgumentException`.

| Class and method | Does |
|------------------|------|
| `ComposioClient.listApps()` | `GET /apps`, as `ComposioApp(name, description, logoUrl, connected)` records |
| `ComposioClient.listTools(app)` | `GET /actions?appNames=<app>`, as `ComposioTool(name, description, appName, inputSchema)` records |
| `ComposioClient.executeTool(action, input)` | `POST /actions/<action>/execute` with `actionName` and `input`; returns the response body as a `JsonNode` |
| `ComposioClient.getConnectionStatus(app)` | `GET /connectedAccounts?appNames=<app>`; connected when the list is not empty, with the first account's `authScheme` |
| `ComposioAuthBridge.verifyApiKey()` | `GET /apps`; false when the request fails with HTTP 401 or 403 |
| `ComposioAuthBridge.getAuthHeaders()` | `X-API-Key` and `Accept: application/json` |
| `ComposioDiscovery.discoverTools(app)` | The app's actions as `McpToolDefinition`s |
| `ComposioDiscovery.discoverTools()` | The actions of every app; an app whose list fails is skipped with a warning |
| `ComposioDiscovery.toServerEntries()` | One `McpServerEntry` per app, named `composio-<app>`, with source `composio` |

Any HTTP status of 400 or more becomes an `IOException` with the status and body. The list endpoints accept either a JSON array or an object with an `items` array. The app name goes into the query string as is, so pass a plain app name.

The discovered `McpToolDefinition`s only describe the actions. To serve them from an [MCP server](server.md), write a `ToolProvider` whose `executeTool` calls `ComposioClient.executeTool`. The entries from `toServerEntries()` point at the REST base URL, not at an MCP endpoint, so do not connect an MCP transport to them.

The paths and field names above are what the client sends and reads. They have not been checked against Composio's current API, so confirm them before you depend on this client.

## Database servers

`com.tnsai.mcp.database` has three wrappers. Each takes a connected, initialized `McpClient` and calls one tool per method. Start and connect the server yourself with a [transport](transports.md).

| Class | Method | Tool called | Arguments |
|-------|--------|-------------|-----------|
| `PostgresMcpClient` | `executeQuery(sql)` | `execute_sql` | `query` |
| | `listTables()` | `list_tables` | none |
| | `describeTable(table)` | `describe_table` | `table` |
| `Neo4jMcpClient` | `executeCypher(query)` | `execute_cypher` | `query` |
| | `listNodeLabels()` | `list_node_labels` | none |
| | `getSchema()` | `get_schema` | none |
| `GoogleMcpToolboxClient` | `executeQuery(query)` | `execute_query` | `query` |
| | `listDatabases()` | `list_databases` | none |
| | `describeTable(table)` | `describe_table` | `table` |

Each method returns what `McpClient.executeTool` returns: the result's `content` array. A null or blank query or table name throws `IllegalArgumentException`. `isAvailable()` is true when the client is connected and initialized, and `getMcpClient()` returns it.

The tool names are fixed. If your server names its tools differently, call `client.executeTool` directly; run `client.listTools()` to see the names.

## Stripe

`StripeMcpClient` wraps a connected `McpClient` the same way.

| Method | Tool called | Arguments |
|--------|-------------|-----------|
| `createPaymentIntent(amount, currency)` | `create_payment_intent` | `amount`, `currency` |
| `listPayments(limit)` | `list_payment_intents` | `limit` |
| `getBalance()` | `get_balance` | none |
| `listCustomers(limit)` | `list_customers` | `limit` |
| `createCustomer(name, email)` | `create_customer` | `name`, `email` |

`StripePaymentTool` puts the five operations behind one call. `execute(operation, params)` matches the operation name ignoring case; `listOperations()` lists `create_payment_intent`, `list_payments`, `get_balance`, `list_customers` and `create_customer`. An unknown operation throws `IllegalArgumentException`. The `limit` defaults to 10. A missing `amount` throws `NumberFormatException`, and a missing `name`, `email` or `currency` is sent as the string `null`. `getName()` is `stripe_payment`, but the class is not a TnsAI tool; to give it to an agent, call it from your own `@Tool` method.

These calls create real payment intents and customers on the account behind the server's key. Use a Stripe test-mode key while you build.

## Related

- [MCP client](client.md)
- [Transports](transports.md)
- [MCP server](server.md)
- [Register tools](../tools/register.md)
