@x402storage/mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +20 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +84 -0
- package/dist/upload.d.ts +38 -0
- package/dist/upload.js +115 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# x402store-mcp
|
|
2
|
+
|
|
3
|
+
MCP server for storing files permanently on IPFS via [x402.storage](https://x402.storage).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g x402store-mcp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or clone and build locally:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
git clone https://github.com/anthropics/x402store-mcp
|
|
15
|
+
cd x402store-mcp
|
|
16
|
+
npm install
|
|
17
|
+
npm run build
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Configuration
|
|
21
|
+
|
|
22
|
+
### 1. Set up your wallet
|
|
23
|
+
|
|
24
|
+
Export your Base wallet private key:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
export X402_PRIVATE_KEY=0x...
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Need a wallet? Create one at [x402.storage](https://x402.storage).
|
|
31
|
+
|
|
32
|
+
### 2. Configure Claude Desktop
|
|
33
|
+
|
|
34
|
+
Add to your Claude Desktop configuration (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS):
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"mcpServers": {
|
|
39
|
+
"x402store": {
|
|
40
|
+
"command": "node",
|
|
41
|
+
"args": ["/path/to/x402store-mcp/dist/index.js"],
|
|
42
|
+
"env": {
|
|
43
|
+
"X402_PRIVATE_KEY": "0x..."
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or if installed globally:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"mcpServers": {
|
|
55
|
+
"x402store": {
|
|
56
|
+
"command": "x402store-mcp",
|
|
57
|
+
"env": {
|
|
58
|
+
"X402_PRIVATE_KEY": "0x..."
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 3. Restart Claude Desktop
|
|
66
|
+
|
|
67
|
+
Fully quit and restart Claude Desktop for the configuration to take effect.
|
|
68
|
+
|
|
69
|
+
## Usage
|
|
70
|
+
|
|
71
|
+
Once configured, you can ask Claude to store files:
|
|
72
|
+
|
|
73
|
+
> "Store the file /path/to/document.pdf permanently"
|
|
74
|
+
|
|
75
|
+
Claude will use the `store_file` tool to upload the file and return the permanent IPFS URL.
|
|
76
|
+
|
|
77
|
+
## Tool
|
|
78
|
+
|
|
79
|
+
### store_file
|
|
80
|
+
|
|
81
|
+
Store a file permanently on IPFS.
|
|
82
|
+
|
|
83
|
+
**Input:**
|
|
84
|
+
- `file_path` (string): Absolute or relative path to the file
|
|
85
|
+
|
|
86
|
+
**Output:**
|
|
87
|
+
- Permanent IPFS gateway URL (e.g., `https://x402.storage/bafybeig...`)
|
|
88
|
+
|
|
89
|
+
**Errors:**
|
|
90
|
+
- File not found
|
|
91
|
+
- Insufficient balance (fund wallet at x402.storage)
|
|
92
|
+
- Network error
|
|
93
|
+
|
|
94
|
+
## Cost
|
|
95
|
+
|
|
96
|
+
Each upload costs $0.01 USDC on Base. Files are stored permanently.
|
|
97
|
+
|
|
98
|
+
## License
|
|
99
|
+
|
|
100
|
+
MIT
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment validation for x402store MCP server
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Validates required environment variables.
|
|
6
|
+
* Returns error message if X402_PRIVATE_KEY is missing, null if valid.
|
|
7
|
+
*/
|
|
8
|
+
export declare function validateEnvironment(): string | null;
|
|
9
|
+
/**
|
|
10
|
+
* Gets the private key from environment.
|
|
11
|
+
* Should only be called after validateEnvironment() returns null.
|
|
12
|
+
*/
|
|
13
|
+
export declare function getPrivateKey(): `0x${string}`;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment validation for x402store MCP server
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Validates required environment variables.
|
|
6
|
+
* Returns error message if X402_PRIVATE_KEY is missing, null if valid.
|
|
7
|
+
*/
|
|
8
|
+
export function validateEnvironment() {
|
|
9
|
+
if (!process.env.X402_PRIVATE_KEY) {
|
|
10
|
+
return 'X402_PRIVATE_KEY not set. Export your Base wallet private key: export X402_PRIVATE_KEY=0x... Need a wallet? Create one at https://x402.storage';
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Gets the private key from environment.
|
|
16
|
+
* Should only be called after validateEnvironment() returns null.
|
|
17
|
+
*/
|
|
18
|
+
export function getPrivateKey() {
|
|
19
|
+
return process.env.X402_PRIVATE_KEY;
|
|
20
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
import { validateEnvironment, getPrivateKey } from './config.js';
|
|
7
|
+
import { uploadFile, FileNotFoundError, UploadError } from './upload.js';
|
|
8
|
+
// Create MCP server
|
|
9
|
+
const server = new McpServer({
|
|
10
|
+
name: 'x402store',
|
|
11
|
+
version: '1.0.0',
|
|
12
|
+
});
|
|
13
|
+
// Register store_file tool
|
|
14
|
+
server.tool('store_file', 'Store a file permanently on IPFS via x402.storage. Returns the permanent gateway URL.', {
|
|
15
|
+
file_path: z
|
|
16
|
+
.string()
|
|
17
|
+
.describe('Absolute or relative path to the file to upload'),
|
|
18
|
+
}, async ({ file_path }) => {
|
|
19
|
+
// Validate environment first
|
|
20
|
+
const envError = validateEnvironment();
|
|
21
|
+
if (envError) {
|
|
22
|
+
return {
|
|
23
|
+
content: [{ type: 'text', text: `Error: ${envError}` }],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const resolvedPath = resolve(file_path);
|
|
28
|
+
const privateKey = getPrivateKey();
|
|
29
|
+
const result = await uploadFile(resolvedPath, privateKey);
|
|
30
|
+
return {
|
|
31
|
+
content: [
|
|
32
|
+
{
|
|
33
|
+
type: 'text',
|
|
34
|
+
text: result.gateway,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (error instanceof FileNotFoundError) {
|
|
41
|
+
return {
|
|
42
|
+
content: [{ type: 'text', text: `Error: File not found: ${file_path}` }],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (error instanceof UploadError) {
|
|
46
|
+
if (error.type === 'insufficient_balance') {
|
|
47
|
+
return {
|
|
48
|
+
content: [
|
|
49
|
+
{
|
|
50
|
+
type: 'text',
|
|
51
|
+
text: 'Error: Insufficient balance. Fund your wallet at https://x402.storage or transfer USDC on Base.',
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (error.type === 'network_error') {
|
|
57
|
+
return {
|
|
58
|
+
content: [
|
|
59
|
+
{ type: 'text', text: 'Error: Network error. Check your connection and try again.' },
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
content: [{ type: 'text', text: `Error: Upload failed: ${error.message}` }],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const message = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
68
|
+
return {
|
|
69
|
+
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
// Run server with stdio transport
|
|
74
|
+
async function main() {
|
|
75
|
+
const transport = new StdioServerTransport();
|
|
76
|
+
await server.connect(transport);
|
|
77
|
+
// Note: MCP servers must not write to stdout (breaks JSON-RPC)
|
|
78
|
+
// Use console.error for any debugging
|
|
79
|
+
console.error('x402store MCP server running on stdio');
|
|
80
|
+
}
|
|
81
|
+
main().catch((error) => {
|
|
82
|
+
console.error('Fatal error:', error);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
});
|
package/dist/upload.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload logic with x402 payment integration
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Error thrown when file doesn't exist
|
|
6
|
+
*/
|
|
7
|
+
export declare class FileNotFoundError extends Error {
|
|
8
|
+
constructor(filePath: string);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Types of upload errors
|
|
12
|
+
*/
|
|
13
|
+
export type UploadErrorType = 'network_error' | 'payment_failed' | 'upload_failed' | 'insufficient_balance';
|
|
14
|
+
/**
|
|
15
|
+
* Error thrown during upload
|
|
16
|
+
*/
|
|
17
|
+
export declare class UploadError extends Error {
|
|
18
|
+
readonly type: UploadErrorType;
|
|
19
|
+
constructor(message: string, type: UploadErrorType);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Response from the upload API
|
|
23
|
+
*/
|
|
24
|
+
interface UploadResponse {
|
|
25
|
+
cid: string;
|
|
26
|
+
gateway: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Uploads a file to api.x402.storage with automatic payment handling
|
|
30
|
+
*
|
|
31
|
+
* @param filePath - Path to the file to upload
|
|
32
|
+
* @param privateKey - Private key for x402 payment signing
|
|
33
|
+
* @returns Object containing CID and gateway URL
|
|
34
|
+
* @throws FileNotFoundError if file doesn't exist
|
|
35
|
+
* @throws UploadError on network, payment, or upload failures
|
|
36
|
+
*/
|
|
37
|
+
export declare function uploadFile(filePath: string, privateKey: string): Promise<UploadResponse>;
|
|
38
|
+
export {};
|
package/dist/upload.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload logic with x402 payment integration
|
|
3
|
+
*/
|
|
4
|
+
import { x402Client } from '@x402/core/client';
|
|
5
|
+
import { x402HTTPClient } from '@x402/core/http';
|
|
6
|
+
import { ExactEvmScheme } from '@x402/evm/exact/client';
|
|
7
|
+
import { wrapFetchWithPayment } from '@x402/fetch';
|
|
8
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
9
|
+
import { openAsBlob } from 'node:fs';
|
|
10
|
+
import { stat } from 'node:fs/promises';
|
|
11
|
+
import { basename } from 'node:path';
|
|
12
|
+
/**
|
|
13
|
+
* Error thrown when file doesn't exist
|
|
14
|
+
*/
|
|
15
|
+
export class FileNotFoundError extends Error {
|
|
16
|
+
constructor(filePath) {
|
|
17
|
+
super(`File not found: ${filePath}`);
|
|
18
|
+
this.name = 'FileNotFoundError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Error thrown during upload
|
|
23
|
+
*/
|
|
24
|
+
export class UploadError extends Error {
|
|
25
|
+
type;
|
|
26
|
+
constructor(message, type) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = 'UploadError';
|
|
29
|
+
this.type = type;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Creates an x402 payment client with the given private key
|
|
34
|
+
*/
|
|
35
|
+
function createPaymentClient(privateKey) {
|
|
36
|
+
const signer = privateKeyToAccount(privateKey);
|
|
37
|
+
const coreClient = new x402Client().register('eip155:*', new ExactEvmScheme(signer));
|
|
38
|
+
return new x402HTTPClient(coreClient);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Uploads a file to api.x402.storage with automatic payment handling
|
|
42
|
+
*
|
|
43
|
+
* @param filePath - Path to the file to upload
|
|
44
|
+
* @param privateKey - Private key for x402 payment signing
|
|
45
|
+
* @returns Object containing CID and gateway URL
|
|
46
|
+
* @throws FileNotFoundError if file doesn't exist
|
|
47
|
+
* @throws UploadError on network, payment, or upload failures
|
|
48
|
+
*/
|
|
49
|
+
export async function uploadFile(filePath, privateKey) {
|
|
50
|
+
// Check file exists
|
|
51
|
+
try {
|
|
52
|
+
await stat(filePath);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
throw new FileNotFoundError(filePath);
|
|
56
|
+
}
|
|
57
|
+
// Create payment client and wrap fetch
|
|
58
|
+
const client = createPaymentClient(privateKey);
|
|
59
|
+
const fetchWithPayment = wrapFetchWithPayment(fetch, client);
|
|
60
|
+
// Read file as blob
|
|
61
|
+
const blob = await openAsBlob(filePath);
|
|
62
|
+
const fileName = basename(filePath);
|
|
63
|
+
// Create FormData with file
|
|
64
|
+
const formData = new FormData();
|
|
65
|
+
formData.append('file', blob, fileName);
|
|
66
|
+
// Upload to api.x402.storage (root endpoint, not /store)
|
|
67
|
+
let response;
|
|
68
|
+
try {
|
|
69
|
+
response = await fetchWithPayment('https://api.x402.storage', {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
body: formData,
|
|
72
|
+
// Do NOT set Content-Type header - let Node.js set boundary
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
// Network or payment errors during fetch
|
|
77
|
+
const message = error instanceof Error ? error.message : 'Unknown network error';
|
|
78
|
+
// Check for payment-related errors
|
|
79
|
+
if (message.toLowerCase().includes('insufficient')) {
|
|
80
|
+
throw new UploadError(message, 'insufficient_balance');
|
|
81
|
+
}
|
|
82
|
+
if (message.toLowerCase().includes('payment') ||
|
|
83
|
+
message.toLowerCase().includes('402')) {
|
|
84
|
+
throw new UploadError(message, 'payment_failed');
|
|
85
|
+
}
|
|
86
|
+
throw new UploadError(message, 'network_error');
|
|
87
|
+
}
|
|
88
|
+
// Handle non-OK responses
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
let errorMessage = `Upload failed: ${response.status} ${response.statusText}`;
|
|
91
|
+
try {
|
|
92
|
+
const errorBody = (await response.json());
|
|
93
|
+
if (errorBody.error) {
|
|
94
|
+
errorMessage = errorBody.error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Ignore JSON parse errors, use status message
|
|
99
|
+
}
|
|
100
|
+
// Determine error type from status code
|
|
101
|
+
if (response.status === 402) {
|
|
102
|
+
throw new UploadError(errorMessage, 'payment_failed');
|
|
103
|
+
}
|
|
104
|
+
if (response.status >= 500) {
|
|
105
|
+
throw new UploadError(errorMessage, 'network_error');
|
|
106
|
+
}
|
|
107
|
+
throw new UploadError(errorMessage, 'upload_failed');
|
|
108
|
+
}
|
|
109
|
+
// Parse successful response
|
|
110
|
+
const result = (await response.json());
|
|
111
|
+
return {
|
|
112
|
+
cid: result.cid,
|
|
113
|
+
gateway: result.gateway,
|
|
114
|
+
};
|
|
115
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@x402storage/mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for x402.storage file uploads",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/rawgroundbeef/x402.storage"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"x402-mcp": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"files": ["dist"],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"keywords": ["x402", "ipfs", "storage", "mcp", "claude"],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18.0.0"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@modelcontextprotocol/sdk": "^1.25.0",
|
|
25
|
+
"@x402/core": "^2.2.0",
|
|
26
|
+
"@x402/evm": "latest",
|
|
27
|
+
"@x402/fetch": "latest",
|
|
28
|
+
"viem": "^2.21.0",
|
|
29
|
+
"zod": "^3.24.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^20.0.0",
|
|
33
|
+
"typescript": "^5.7.0"
|
|
34
|
+
}
|
|
35
|
+
}
|