Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tough-pianos-poke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ton/mcp': patch
---

Persist wallet secrets in protected files and allow creating the MCP server directly from a WalletKit signer
54 changes: 38 additions & 16 deletions packages/mcp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,29 @@ HTTP mode keeps a separate MCP session/transport per client session id, so multi
| `AGENTIC_CALLBACK_HOST` | `127.0.0.1` | Host for the local callback server in stdio mode |
| `AGENTIC_CALLBACK_PORT` | random free port | Port for the local callback server in stdio mode |

## Key Storage

`@ton/mcp` stores secrets differently depending on the runtime mode.

### Registry mode

- Wallet metadata is stored in `~/.config/ton/config.json` by default, or at `TON_CONFIG_PATH` if provided.
- Mnemonics and private keys are not kept inline in the registry after persistence. The config stores only a `secret_file` reference.
- Secret material is written into separate files under `<config-dir>/private-keys/...`, including wallet secrets, pending agentic deployment secrets, and pending agentic key rotation secrets.
- The config file and every secret file are written with strict filesystem permissions: files use `0600`, directories use `0700`.
- Both config and secret files go through the same `protected-file` layer, so the raw bytes on disk do not contain the plaintext mnemonic or private key.
- Legacy inline secrets from older config formats are still readable on load and are moved to `secret_file` storage on the next save or migration.
- When a wallet, pending deployment, or pending rotation is removed, orphaned secret files are deleted as part of the config transition.

### Single-wallet mode

- If you start the server with `MNEMONIC` or `PRIVATE_KEY`, the wallet is created directly from those values.
- In this mode the secret is not persisted by `@ton/mcp` into the local registry. It is kept in process memory for the lifetime of the MCP server.

### Security note

The built-in `protected-file` wrapper helps avoid writing mnemonics and private keys to disk in readable form, but it is not a replacement for an OS keychain, HSM, or external KMS. If you need stronger host-level secret protection, provide credentials through your own secret-management layer in single-wallet/serverless mode.

## Available Tools

In registry mode, wallet-scoped tools below also accept optional `walletSelector`. If omitted, the active wallet is used.
Expand Down Expand Up @@ -436,26 +459,25 @@ The package also exports a programmatic API for building custom MCP servers:

```typescript
import { createTonWalletMCP } from '@ton/mcp';
import { Signer, WalletV5R1Adapter, TonWalletKit, MemoryStorageAdapter, Network } from '@ton/walletkit';
import { Signer } from '@ton/walletkit';

// Initialize TonWalletKit
const network = Network.mainnet();
const kit = new TonWalletKit({
networks: { [network.chainId]: {} },
storage: new MemoryStorageAdapter(),
});
await kit.waitForReady();

// Create wallet from mnemonic
// Create signer from mnemonic
const signer = await Signer.fromMnemonic(mnemonic, { type: 'ton' });
const walletAdapter = await WalletV5R1Adapter.create(signer, {
client: kit.getApiClient(network),
network,

// Create MCP server directly from signer
const server = await createTonWalletMCP({
signer,
network: 'mainnet',
walletVersion: 'v5r1',
});
const wallet = await kit.addWallet(walletAdapter);
```

If you already have a custom wallet adapter, you can still pass it directly:

```typescript
import { createTonWalletMCP } from '@ton/mcp';

// Create MCP server
const server = await createTonWalletMCP({ wallet });
const server = await createTonWalletMCP({ wallet: walletAdapter });
```

The same factory also supports registry mode:
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -282,5 +282,5 @@ Parameters:
- Registry mode uses the local TON config file from `~/.config/ton/config.json` or `TON_CONFIG_PATH`
- Agentic onboarding callback state is persisted in the local config; in stdio mode use `AGENTIC_CALLBACK_BASE_URL` and/or `AGENTIC_CALLBACK_PORT` when you need a stable callback endpoint across restarts
- Registry management responses are sanitized and do not expose mnemonic, private keys, operator private keys, or Toncenter API keys
- Read tools can work with imported agentic wallets without `operator_private_key`; write tools cannot
- Read tools can work with imported agentic wallets without `private_key`; write tools cannot
- **Default flow:** After any send, poll get_transaction_status until completed or failed. User can specify whether to check status.
2 changes: 1 addition & 1 deletion packages/mcp/skills/ton-manage-wallets/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,6 @@ Manage the local wallet registry and perform advanced agentic wallet operations
- Use available shell/browser tools to open dashboard URLs for the user whenever possible
- For confirmations and small option sets, prefer the host client's structured confirmation/choice UI when available; otherwise use a short natural-language yes/no prompt and never require an exact magic word
- Registry data is stored in `~/.config/ton/config.json` (or `TON_CONFIG_PATH`)
- Read tools work with imported agentic wallets that don't yet have an `operator_private_key`; write tools require it
- Read tools work with imported agentic wallets that don't yet have a `private_key`; write tools require it
- Management tool responses never expose private keys, mnemonics, or API keys
- To create a brand new agentic wallet, use the `ton-create-wallet` skill instead
6 changes: 3 additions & 3 deletions packages/mcp/src/__tests__/AgenticOnboardingService.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('AgenticOnboardingService', () => {
const pendingDeployment: PendingAgenticDeployment = {
id: 'setup-1',
network: 'mainnet',
operator_private_key: '0xpriv',
secret_file: '/tmp/setup-1.private-key',
operator_public_key: '0xfeed',
name: 'Agent Alpha',
source: 'MCP flow',
Expand All @@ -33,7 +33,7 @@ describe('AgenticOnboardingService', () => {
network: 'mainnet',
address: 'UQBynBO23ywHy_CgarY9NK9FTz0yDsG82PtcbSTQgGoXwnZF',
owner_address: 'UQAcIXCxCd_gAqQ8RK0UA9vvlVA7wWjV41l2URKVxaMVLeM5',
operator_private_key: '0xpriv',
secret_file: '/tmp/agent-1.private-key',
operator_public_key: '0xfeed',
source: 'MCP flow',
collection_address: 'EQByQ19qvWxW7VibSbGEgZiYMqilHY5y1a_eeSL2VaXhfy07',
Expand Down Expand Up @@ -94,7 +94,7 @@ describe('AgenticOnboardingService', () => {

expect(registry.createPendingAgenticSetup).toHaveBeenCalledWith({
network: 'mainnet',
operatorPrivateKey: expect.stringMatching(/^0x[0-9a-f]+$/i),
privateKey: expect.stringMatching(/^0x[0-9a-f]+$/i),
operatorPublicKey: expect.stringMatching(/^0x[0-9a-f]+$/i),
name: 'Agent Alpha',
source: 'MCP flow',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,11 @@ describe('AgenticSetupSessionManager', () => {

await manager.createSession('setup-3');
manager.markCompleted('setup-3');
expect(manager.getSession('setup-3')).toBeNull();
expect(manager.getSession('setup-3')).toBeUndefined();

await manager.createSession('setup-4');
manager.cancelSession('setup-4');
expect(manager.getSession('setup-4')).toBeNull();
expect(manager.getSession('setup-4')).toBeUndefined();
});

it('restores persisted callback payloads and callback urls from config-backed store', async () => {
Expand Down
Loading
Loading