@anaxer/mcp
Advanced tools
+334
| #!/usr/bin/env node | ||
| // src/index.ts | ||
| import { connect } from "@anaxer/sdk"; | ||
| import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; | ||
| // src/config.ts | ||
| var ConfigError = class extends Error { | ||
| constructor(message) { | ||
| super(message); | ||
| this.name = "ConfigError"; | ||
| } | ||
| }; | ||
| function parseConfig(env = process.env) { | ||
| const apiKey = env.ANAXER_API_KEY?.trim(); | ||
| if (!apiKey) { | ||
| throw new ConfigError( | ||
| "ANAXER_API_KEY is required. Set it in your MCP client env config (e.g. Claude Desktop / Cursor mcpServers.*.env)." | ||
| ); | ||
| } | ||
| const baseUrl = env.ANAXER_BASE_URL?.trim() || void 0; | ||
| const wsUrl = env.ANAXER_WS_URL?.trim() || void 0; | ||
| return { apiKey, baseUrl, wsUrl }; | ||
| } | ||
| // src/server.ts | ||
| import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; | ||
| // src/tools/markets.ts | ||
| import { z } from "zod"; | ||
| // src/format.ts | ||
| import { AnaxerError } from "@anaxer/sdk"; | ||
| function successResult(payload) { | ||
| return { | ||
| content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] | ||
| }; | ||
| } | ||
| function errorResult(err) { | ||
| if (err instanceof AnaxerError) { | ||
| return { | ||
| isError: true, | ||
| content: [{ type: "text", text: `${err.code}: ${err.message}` }] | ||
| }; | ||
| } | ||
| const message = err instanceof Error ? err.message : String(err); | ||
| return { | ||
| isError: true, | ||
| content: [{ type: "text", text: `internal: ${message}` }] | ||
| }; | ||
| } | ||
| async function runTool(fn) { | ||
| try { | ||
| return successResult(await fn()); | ||
| } catch (err) { | ||
| return errorResult(err); | ||
| } | ||
| } | ||
| // src/tools/markets.ts | ||
| var listLimitSchema = z.number().int().min(1).max(200).default(20).describe( | ||
| "Rows per page; default 20 to conserve context \u2014 raise only if needed" | ||
| ); | ||
| function registerMarketTools(server, client) { | ||
| server.registerTool( | ||
| "list_creations", | ||
| { | ||
| description: 'One page of recent token creations (launches). Optional singular `source` (e.g. "pump_fun") \u2014 NOT an array; live multi-source use tail_stream with `sources`. Use excludeMayhem=true to drop mayhem-mode launches. To get more, call again with `cursor` set to the previous `next`; stop when `next` is null.', | ||
| inputSchema: { | ||
| source: z.string().optional().describe('Single source, e.g. "pump_fun" (NOT an array)'), | ||
| limit: listLimitSchema, | ||
| from: z.union([z.number(), z.string()]).optional(), | ||
| to: z.union([z.number(), z.string()]).optional(), | ||
| excludeMayhem: z.boolean().optional(), | ||
| cursor: z.string().optional().describe("Pass the previous response's `next` for the next page") | ||
| } | ||
| }, | ||
| async ({ source, limit, from, to, excludeMayhem, cursor }) => runTool(async () => { | ||
| const page = await client.creations({ | ||
| source, | ||
| limit, | ||
| from, | ||
| to, | ||
| excludeMayhem, | ||
| cursor | ||
| }); | ||
| return { data: page.data, next: page.next, window: page.window }; | ||
| }) | ||
| ); | ||
| server.registerTool( | ||
| "list_graduations", | ||
| { | ||
| description: "One page of recent graduations/migrations. Optional singular `source` (NOT an array). Use minLiquiditySol / excludeMayhem to filter junk. To get more, call again with `cursor` set to the previous `next`; stop when `next` is null.", | ||
| inputSchema: { | ||
| source: z.string().optional().describe('Single source, e.g. "pump_fun" (NOT an array)'), | ||
| limit: listLimitSchema, | ||
| from: z.union([z.number(), z.string()]).optional(), | ||
| to: z.union([z.number(), z.string()]).optional(), | ||
| excludeMayhem: z.boolean().optional(), | ||
| minLiquiditySol: z.number().optional(), | ||
| cursor: z.string().optional().describe("Pass the previous response's `next` for the next page") | ||
| } | ||
| }, | ||
| async ({ source, limit, from, to, excludeMayhem, minLiquiditySol, cursor }) => runTool(async () => { | ||
| const page = await client.graduations({ | ||
| source, | ||
| limit, | ||
| from, | ||
| to, | ||
| excludeMayhem, | ||
| minLiquiditySol, | ||
| cursor | ||
| }); | ||
| return { data: page.data, next: page.next, window: page.window }; | ||
| }) | ||
| ); | ||
| server.registerTool( | ||
| "get_launchpad_stats", | ||
| { | ||
| description: "Aggregate launchpad stats (creations/graduations counts) over a rolling window. Use for 'which launchpads are hot'. Optional windowHours.", | ||
| inputSchema: { | ||
| windowHours: z.number().int().positive().optional() | ||
| } | ||
| }, | ||
| async ({ windowHours }) => runTool(() => client.launchpads.stats({ windowHours })) | ||
| ); | ||
| } | ||
| // src/tools/tail.ts | ||
| import { z as z2 } from "zod"; | ||
| var CHANNELS = ["trades", "creations", "graduations", "prices"]; | ||
| function registerTailTool(server, client) { | ||
| server.registerTool( | ||
| "tail_stream", | ||
| { | ||
| description: "Collect a bounded batch of live WebSocket events, then return and unsubscribe. Always terminates: ends when maxEvents (default 20, cap 100) or timeoutMs (default 10000, cap 30000) hits first. filters is a per-channel bag using WS shapes \u2014 note `sources` is an ARRAY (unlike REST list tools' singular `source`). Valid keys: trades: sources, mints, wallets, minVolumeUsd, maxVolumeUsd; creations: sources, enriched, excludeMayhem; graduations: sources, excludeMayhem, minLiquiditySol; prices: sources, mints. No transfers channel in v1. Use for short live watches / summaries, not history (history \u2192 list_* tools).", | ||
| inputSchema: { | ||
| channel: z2.enum(CHANNELS), | ||
| filters: z2.record(z2.unknown()).optional().describe( | ||
| "Per-channel filter bag (WS filter shape). See the tool description for the valid keys per channel; uses `sources` (array), not `source`." | ||
| ), | ||
| maxEvents: z2.number().int().min(1).max(100).default(20), | ||
| timeoutMs: z2.number().int().min(1e3).max(3e4).default(1e4) | ||
| } | ||
| }, | ||
| async ({ channel, filters, maxEvents, timeoutMs }) => { | ||
| try { | ||
| const result = await collectTail(client, { | ||
| channel, | ||
| filters, | ||
| maxEvents, | ||
| timeoutMs | ||
| }); | ||
| return successResult(result); | ||
| } catch (err) { | ||
| return errorResult(err); | ||
| } | ||
| } | ||
| ); | ||
| } | ||
| async function collectTail(client, opts) { | ||
| const { channel, filters, maxEvents, timeoutMs } = opts; | ||
| const events = []; | ||
| const sub = client.stream(channel, filters); | ||
| let timer; | ||
| let settled = false; | ||
| let onClientError; | ||
| try { | ||
| const endedBy = await new Promise((resolve, reject) => { | ||
| const settle = (reason) => { | ||
| if (settled) return; | ||
| settled = true; | ||
| resolve(reason); | ||
| }; | ||
| const fail = (err) => { | ||
| if (settled) return; | ||
| settled = true; | ||
| reject(err); | ||
| }; | ||
| const onData = (data) => { | ||
| if (settled) return; | ||
| events.push(data); | ||
| if (events.length >= maxEvents) settle("maxEvents"); | ||
| }; | ||
| const onSubError = (err) => { | ||
| fail(err); | ||
| }; | ||
| onClientError = (err) => { | ||
| fail(err); | ||
| }; | ||
| sub.on("data", onData); | ||
| sub.on("error", onSubError); | ||
| client.on("error", onClientError); | ||
| timer = setTimeout(() => settle("timeout"), timeoutMs); | ||
| }); | ||
| return { | ||
| channel, | ||
| endedBy, | ||
| count: events.length, | ||
| events | ||
| }; | ||
| } finally { | ||
| if (timer !== void 0) clearTimeout(timer); | ||
| if (onClientError) { | ||
| client.off("error", onClientError); | ||
| } | ||
| sub.close(); | ||
| } | ||
| } | ||
| // src/tools/tokens.ts | ||
| import { z as z3 } from "zod"; | ||
| var mintSchema = z3.string().min(32).max(44).describe("Base58 mint address"); | ||
| var listLimitSchema2 = z3.number().int().min(1).max(200).default(20).describe( | ||
| "Rows per page; default 20 to conserve context \u2014 raise only if needed" | ||
| ); | ||
| function registerTokenTools(server, client) { | ||
| server.registerTool( | ||
| "get_token_metadata", | ||
| { | ||
| description: "Name, symbol, supply, and socials for one mint. Use for 'what is this token' / metadata lookups. 404 \u2192 mint not in Anaxer yet (never observed or resolve failed).", | ||
| inputSchema: { mint: mintSchema } | ||
| }, | ||
| async ({ mint }) => runTool(() => client.tokens.get(mint)) | ||
| ); | ||
| server.registerTool( | ||
| "get_tokens_metadata", | ||
| { | ||
| description: "Batch metadata for up to 30 mints. Missing/never-observed mints are omitted from the array (not errored). Prefer over many get_token_metadata calls.", | ||
| inputSchema: { | ||
| mints: z3.array(mintSchema).min(1).max(30).describe("Up to 30 base58 mints") | ||
| } | ||
| }, | ||
| async ({ mints }) => runTool(() => client.tokens.batch(mints)) | ||
| ); | ||
| server.registerTool( | ||
| "get_token_price", | ||
| { | ||
| description: "Latest USD + SOL price and market cap for one mint. Use for 'what's X worth now'. 404 \u2192 the mint has no live price.", | ||
| inputSchema: { mint: mintSchema } | ||
| }, | ||
| async ({ mint }) => runTool(() => client.tokens.price(mint)) | ||
| ); | ||
| server.registerTool( | ||
| "get_token_prices", | ||
| { | ||
| description: "Batch latest prices for up to 30 mints. Missing prices are omitted from the array. Prefer over many get_token_price calls.", | ||
| inputSchema: { | ||
| mints: z3.array(mintSchema).min(1).max(30).describe("Up to 30 base58 mints") | ||
| } | ||
| }, | ||
| async ({ mints }) => runTool(() => client.tokens.batchPrice(mints)) | ||
| ); | ||
| server.registerTool( | ||
| "list_token_trades", | ||
| { | ||
| description: 'One page of recent swaps for a mint (REST history, not a live tail). Optional singular `source` filter (e.g. "pump_fun") \u2014 NOT an array; for live multi-source filtering use tail_stream with `sources`. To get more, call again with `cursor` set to the previous `next`; stop when `next` is null. Prefer limit \u2264 20 (default) to conserve context.', | ||
| inputSchema: { | ||
| mint: mintSchema, | ||
| limit: listLimitSchema2, | ||
| from: z3.union([z3.number(), z3.string()]).optional().describe("Unix ms or ISO-8601 start bound"), | ||
| to: z3.union([z3.number(), z3.string()]).optional().describe("Unix ms or ISO-8601 end bound"), | ||
| source: z3.string().optional().describe('Single source, e.g. "pump_fun" (NOT an array)'), | ||
| cursor: z3.string().optional().describe("Pass the previous response's `next` for the next page") | ||
| } | ||
| }, | ||
| async ({ mint, limit, from, to, source, cursor }) => runTool(async () => { | ||
| const page = await client.tokens.trades(mint, { | ||
| limit, | ||
| from, | ||
| to, | ||
| source, | ||
| cursor | ||
| }); | ||
| return { data: page.data, next: page.next, window: page.window }; | ||
| }) | ||
| ); | ||
| } | ||
| // src/server.ts | ||
| function buildServer(client) { | ||
| const server = new McpServer({ | ||
| name: "anaxer", | ||
| version: "0.1.0" | ||
| }); | ||
| registerTokenTools(server, client); | ||
| registerMarketTools(server, client); | ||
| registerTailTool(server, client); | ||
| return server; | ||
| } | ||
| // src/index.ts | ||
| async function main() { | ||
| let config; | ||
| try { | ||
| config = parseConfig(); | ||
| } catch (err) { | ||
| const message = err instanceof ConfigError ? err.message : err instanceof Error ? err.message : String(err); | ||
| console.error(message); | ||
| process.exit(1); | ||
| } | ||
| const client = connect({ | ||
| apiKey: config.apiKey, | ||
| baseUrl: config.baseUrl, | ||
| wsUrl: config.wsUrl | ||
| }); | ||
| const server = buildServer(client); | ||
| const transport = new StdioServerTransport(); | ||
| const shutdown = async () => { | ||
| try { | ||
| await client.close(); | ||
| } catch (err) { | ||
| console.error( | ||
| "anaxer-mcp: error during client.close():", | ||
| err instanceof Error ? err.message : err | ||
| ); | ||
| } | ||
| process.exit(0); | ||
| }; | ||
| process.once("SIGINT", () => { | ||
| void shutdown(); | ||
| }); | ||
| process.once("SIGTERM", () => { | ||
| void shutdown(); | ||
| }); | ||
| await server.connect(transport); | ||
| } | ||
| main().catch((err) => { | ||
| console.error( | ||
| "anaxer-mcp: fatal:", | ||
| err instanceof Error ? err.message : err | ||
| ); | ||
| process.exit(1); | ||
| }); |
+25
-20
| { | ||
| "name": "@anaxer/mcp", | ||
| "version": "0.0.1", | ||
| "description": "Official Anaxer MCP server for Claude and Cursor. Not available yet - this is a placeholder release that reserves the package name.", | ||
| "version": "0.1.0", | ||
| "description": "Official MCP server for Anaxer — query Solana DEX data from Claude Desktop, Claude Code, Cursor, and other MCP hosts.", | ||
| "license": "MIT", | ||
| "type": "module", | ||
| "bin": { | ||
| "anaxer-mcp": "bin/cli.js" | ||
| "anaxer-mcp": "./dist/index.js" | ||
| }, | ||
| "files": [ | ||
| "bin", | ||
| "README.md", | ||
| "LICENSE" | ||
| ], | ||
| "license": "MIT", | ||
| "homepage": "https://anaxer.com/docs", | ||
| "bugs": "https://anaxer.com/contact", | ||
| "keywords": [ | ||
| "mcp", | ||
| "model-context-protocol", | ||
| "solana", | ||
| "claude", | ||
| "cursor", | ||
| "pump.fun" | ||
| ], | ||
| "files": ["dist", "README.md"], | ||
| "engines": { | ||
| "node": ">=18" | ||
| "node": ">=20" | ||
| }, | ||
| "scripts": { | ||
| "build": "tsup && node ./scripts/assert-no-stdout-logs.mjs", | ||
| "assert-stdio": "node ./scripts/assert-no-stdout-logs.mjs", | ||
| "typecheck": "tsc --noEmit", | ||
| "test": "vitest run", | ||
| "prepublishOnly": "npm run build" | ||
| }, | ||
| "dependencies": { | ||
| "@anaxer/sdk": "^0.1.0", | ||
| "@modelcontextprotocol/sdk": "^1.29.0", | ||
| "zod": "^3.23.8" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "^20.16.10", | ||
| "tsup": "^8.5.0", | ||
| "typescript": "^5.6.3", | ||
| "vitest": "^3.2.4" | ||
| }, | ||
| "publishConfig": { | ||
@@ -28,0 +33,0 @@ "access": "public" |
+86
-13
@@ -1,24 +0,97 @@ | ||
| # @anaxer/mcp | ||
| # `@anaxer/mcp` | ||
| > This package name is reserved for the official Anaxer MCP server. It is **not available yet.** | ||
| Official [MCP](https://modelcontextprotocol.io) server for the Anaxer Solana real-time | ||
| data API. Add a few lines to Claude Desktop, Claude Code, or Cursor and the assistant | ||
| can call typed tools over your API key — prices, metadata, creations, graduations, and a | ||
| bounded live stream tail. | ||
| [Anaxer](https://anaxer.com) is a real-time Solana data API — WebSocket streams and REST for Pump.fun, PumpSwap, and more. The real MCP server will let Claude, Cursor, and other MCP-compatible agents query live Solana data directly. | ||
| **Requires Node ≥ 20.** This package wraps [`@anaxer/sdk`](https://www.npmjs.com/package/@anaxer/sdk); | ||
| it adds no gateway or wire behavior. | ||
| This is a placeholder release. It exists only to reserve the `@anaxer/mcp` package name and protect users from a malicious server being published under it before the real one ships — this matters more than most placeholders, since MCP configs commonly run `npx -y @anaxer/mcp`, which installs and executes whatever is published under this name with no confirmation prompt. | ||
| ```bash | ||
| npx -y @anaxer/mcp | ||
| ``` | ||
| Running it prints a clear message and exits — it does not silently pretend to work or implement any part of the MCP protocol: | ||
| ## Setup | ||
| Set `ANAXER_API_KEY` in the MCP host config (never pass the key as a tool argument). | ||
| ### Claude Desktop / Claude Code / Cursor | ||
| ```jsonc | ||
| { | ||
| "mcpServers": { | ||
| "anaxer": { | ||
| "command": "npx", | ||
| "args": ["-y", "@anaxer/mcp"], | ||
| "env": { | ||
| "ANAXER_API_KEY": "sk_live_…" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| ``` | ||
| $ npx -y @anaxer/mcp | ||
| @anaxer/mcp is not available yet. | ||
| This package name is reserved for the official Anaxer MCP server to | ||
| prevent impersonation. Follow progress and get notified at | ||
| https://anaxer.com/docs | ||
| ``` | ||
| **Follow progress:** https://anaxer.com/docs | ||
| **Status:** not yet released. | ||
| Optional: | ||
| | Env | Default | Purpose | | ||
| |---|---|---| | ||
| | `ANAXER_BASE_URL` | `https://api.anaxer.com` | REST base (local gateway: `http://localhost:3010`) | | ||
| | `ANAXER_WS_URL` | derived from base → `…/v1/stream` | Override WebSocket URL | | ||
| ## Tools | ||
| | Tool | What it answers | | ||
| |---|---| | ||
| | `get_token_metadata` | Name/symbol/supply/socials for one mint | | ||
| | `get_tokens_metadata` | Batch metadata (≤30 mints) | | ||
| | `get_token_price` | Latest USD + SOL price / market cap for one mint | | ||
| | `get_token_prices` | Batch prices (≤30 mints) | | ||
| | `list_token_trades` | One page of recent swaps for a mint (REST history) | | ||
| | `list_creations` | One page of recent launches | | ||
| | `list_graduations` | One page of recent graduations | | ||
| | `get_launchpad_stats` | Aggregate launchpad stats over a window | | ||
| | `tail_stream` | Bounded live WS batch (`trades` / `creations` / `graduations` / `prices`) | | ||
| List tools default to `limit: 20` (max 200) to conserve model context. Paginate by calling | ||
| again with `cursor` set to the previous `next`; stop when `next` is null. | ||
| **Spelling:** REST list tools take a singular `source` string. `tail_stream` filters use a | ||
| `sources` **array** (WebSocket filter shape). | ||
| ### `tail_stream` caps | ||
| - `maxEvents` — default `20`, cap `100` | ||
| - `timeoutMs` — default `10_000`, cap `30_000` | ||
| - Ends when **either** cap hits first; always unsubscribes before returning | ||
| Per-channel filter keys: | ||
| - `trades`: `sources`, `mints`, `wallets`, `minVolumeUsd`, `maxVolumeUsd` | ||
| - `creations`: `sources`, `enriched`, `excludeMayhem` | ||
| - `graduations`: `sources`, `excludeMayhem`, `minLiquiditySol` | ||
| - `prices`: `sources`, `mints` | ||
| ## v1 omissions | ||
| These are intentional, not bugs: | ||
| - No `tail_stream("transfers")` and no `list_programs` (same as `@anaxer/sdk` v1) | ||
| - No remote / hosted MCP (Streamable HTTP) — local **stdio** only | ||
| - No gap recovery on reconnect (SDK has none) | ||
| - No MCP resources or prompts — tools only | ||
| ## Troubleshooting | ||
| 1. **Wrong API key on `tail_stream`.** REST tools fail per call and recover as soon as you | ||
| fix `ANAXER_API_KEY`. WebSocket `unauthorized` is **terminal** for the shared socket — | ||
| **restart the MCP server process** (reload the host / re-run `npx @anaxer/mcp`) after | ||
| fixing the key. | ||
| 2. **`subscription_limit`.** Each open `tail_stream` holds one plan subscription until it | ||
| returns. Overlapping tails (or a leaked subscription) on free/low plans can hit the | ||
| cap. The server always `close()`s in `finally`; hosts normally serialize tool calls, so | ||
| this is rare. | ||
| ## License | ||
| MIT |
-11
| #!/usr/bin/env node | ||
| "use strict"; | ||
| console.error( | ||
| "\n@anaxer/mcp is not available yet.\n" + | ||
| "This package name is reserved for the official Anaxer MCP server to\n" + | ||
| "prevent impersonation. Follow progress and get notified at\n" + | ||
| "https://anaxer.com/docs\n" | ||
| ); | ||
| process.exit(1); |
-21
| MIT License | ||
| Copyright (c) 2026 Anaxer | ||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| of this software and associated documentation files (the "Software"), to deal | ||
| in the Software without restriction, including without limitation the rights | ||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| copies of the Software, and to permit persons to whom the Software is | ||
| furnished to do so, subject to the following conditions: | ||
| The above copyright notice and this permission notice shall be included in all | ||
| copies or substantial portions of the Software. | ||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| SOFTWARE. |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
No website
QualityPackage does not have a website.
Trivial Package
Supply chain riskPackages less than 10 lines of code are easily copied into your own project and may not warrant the additional supply chain risk of an external dependency.
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
16010
410.69%328
3544.44%98
292%Yes
NaN3
Infinity%4
Infinity%3
-25%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added