@blockrun/mcp 0.17.0 → 0.18.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.
Files changed (2) hide show
  1. package/dist/index.js +176 -2
  2. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  ImageClient,
15
15
  PriceClient,
16
16
  SolanaLLMClient,
17
+ AnthropicClient,
17
18
  getOrCreateWallet,
18
19
  getOrCreateSolanaWallet,
19
20
  loadSolanaWallet,
@@ -33,6 +34,7 @@ var _priceClient = null;
33
34
  var _freePriceClient = null;
34
35
  var _evmWalletInfo = null;
35
36
  var _solanaClient = null;
37
+ var _anthropicClient = null;
36
38
  function readChainPreference() {
37
39
  for (const file of CHAIN_PREFERENCE_FILES) {
38
40
  try {
@@ -58,6 +60,7 @@ var CHAIN_FILE = path.join(BLOCKRUN_DIR, ".chain");
58
60
  function resetChainCaches() {
59
61
  _evmClient = null;
60
62
  _solanaClient = null;
63
+ _anthropicClient = null;
61
64
  _imageClient = null;
62
65
  _priceClient = null;
63
66
  _freePriceClient = null;
@@ -78,6 +81,12 @@ async function ensureBothWallets() {
78
81
  solana: { address: sol.address, isNew: sol.isNew }
79
82
  };
80
83
  }
84
+ function baseOnlyMessage(capability) {
85
+ if (getChain() === "solana") {
86
+ return `${capability} currently supports Base-chain payment only \u2014 your active chain is Solana. Switch with: blockrun_wallet action:"chain" chain:"base" (switch back later with chain:"solana"). Solana support depends on the @blockrun/llm SDK adding this capability.`;
87
+ }
88
+ return null;
89
+ }
81
90
  function ensureEvmWallet() {
82
91
  if (!_evmWalletInfo) {
83
92
  _evmWalletInfo = getOrCreateWallet();
@@ -108,6 +117,13 @@ function getClient() {
108
117
  }
109
118
  return _evmClient;
110
119
  }
120
+ function getAnthropicClient() {
121
+ if (!_anthropicClient) {
122
+ const privateKey = getOrCreateWalletKey();
123
+ _anthropicClient = new AnthropicClient({ privateKey });
124
+ }
125
+ return _anthropicClient;
126
+ }
111
127
  function getImageClient() {
112
128
  if (!_imageClient) {
113
129
  const privateKey = getOrCreateWalletKey();
@@ -669,6 +685,134 @@ function recordSpending(budget, cost, agentId) {
669
685
  }
670
686
  }
671
687
 
688
+ // src/tools/chat-anthropic.ts
689
+ function isAnthropicModel(model) {
690
+ const id = model.trim();
691
+ return /^anthropic\//i.test(id) || /^claude-/i.test(id);
692
+ }
693
+ function parseDataUri(url) {
694
+ const match = /^data:(image\/(?:jpeg|png|gif|webp));base64,(.+)$/i.exec(url.trim());
695
+ if (!match) return null;
696
+ return {
697
+ type: "base64",
698
+ media_type: match[1].toLowerCase(),
699
+ data: match[2]
700
+ };
701
+ }
702
+ function toAnthropicContent(content) {
703
+ if (typeof content === "string") return content;
704
+ const blocks = [];
705
+ for (const part of content) {
706
+ if (part.type === "text") {
707
+ if (part.text) blocks.push({ type: "text", text: part.text });
708
+ } else if (part.type === "image_url") {
709
+ const url = part.image_url?.url;
710
+ if (!url) continue;
711
+ const base64 = parseDataUri(url);
712
+ const source = base64 ? base64 : { type: "url", url };
713
+ blocks.push({ type: "image", source });
714
+ }
715
+ }
716
+ return blocks;
717
+ }
718
+ function isTextBlock(b) {
719
+ return b.type === "text";
720
+ }
721
+ function isThinkingBlock(b) {
722
+ return b.type === "thinking";
723
+ }
724
+ async function handleAnthropicNative(args) {
725
+ const {
726
+ client,
727
+ model,
728
+ message,
729
+ system,
730
+ messages,
731
+ maxTokens,
732
+ temperature,
733
+ stop,
734
+ thinking,
735
+ budget,
736
+ agentId,
737
+ estimatedCost
738
+ } = args;
739
+ const systemParts = [];
740
+ if (system) systemParts.push(system);
741
+ const apiMessages = [];
742
+ for (const m of messages ?? []) {
743
+ if (m.role === "system") {
744
+ const text = typeof m.content === "string" ? m.content : m.content.filter((p) => p.type === "text").map((p) => p.text).join("\n");
745
+ if (text) systemParts.push(text);
746
+ continue;
747
+ }
748
+ apiMessages.push({ role: m.role, content: toAnthropicContent(m.content) });
749
+ }
750
+ if (message.trim()) apiMessages.push({ role: "user", content: message });
751
+ if (apiMessages.length === 0) {
752
+ return { content: [{ type: "text", text: "No message content to send." }], isError: true };
753
+ }
754
+ let effectiveMax = maxTokens ?? 1024;
755
+ let raisedMaxTokens = false;
756
+ if (thinking && effectiveMax <= thinking.budget_tokens) {
757
+ effectiveMax = thinking.budget_tokens + 1024;
758
+ raisedMaxTokens = true;
759
+ }
760
+ const params = {
761
+ model,
762
+ max_tokens: effectiveMax,
763
+ messages: apiMessages
764
+ };
765
+ if (systemParts.length) params.system = systemParts.join("\n\n");
766
+ if (stop && stop.length) params.stop_sequences = stop;
767
+ if (thinking) {
768
+ params.thinking = { type: "enabled", budget_tokens: thinking.budget_tokens };
769
+ } else if (temperature !== void 0) {
770
+ params.temperature = Math.max(0, Math.min(1, temperature));
771
+ }
772
+ let native;
773
+ try {
774
+ native = await client.messages.create(params);
775
+ } catch (error) {
776
+ const errorMessage = error instanceof Error ? error.message : String(error);
777
+ return { content: [{ type: "text", text: formatError(errorMessage) }], isError: true };
778
+ }
779
+ recordSpending(budget, estimatedCost, agentId);
780
+ const thinkingBlocks = native.content.filter(isThinkingBlock);
781
+ const textBlocks = native.content.filter(isTextBlock);
782
+ const answerText = textBlocks.map((b) => b.text).join("\n");
783
+ const thinkingText = thinkingBlocks.map((b) => b.thinking).join("\n");
784
+ const signaturePresent = thinkingBlocks.some(
785
+ (b) => typeof b.signature === "string" && b.signature.length > 0
786
+ );
787
+ const headerBits = [native.model, "native /v1/messages", `thinking ${thinking ? "on" : "off"}`];
788
+ if (raisedMaxTokens) headerBits.push(`max_tokens\u2192${effectiveMax}`);
789
+ const header = `[${headerBits.join(" | ")}]`;
790
+ const content = [{ type: "text", text: `${header}
791
+
792
+ ${answerText}` }];
793
+ if (thinkingText) {
794
+ content.push({ type: "text", text: `\u{1F9E0} Thinking (signature ${signaturePresent ? "present" : "absent"}):
795
+ ${thinkingText}` });
796
+ }
797
+ return {
798
+ content,
799
+ structuredContent: {
800
+ requested_model: model,
801
+ // Verbatim upstream model id — proof the call hit real Claude with no
802
+ // substitution. Intentionally NOT rewritten back to the requested id.
803
+ model: native.model,
804
+ response: answerText,
805
+ thinking: thinkingText || void 0,
806
+ // Native thinking blocks verbatim, including their original signature.
807
+ thinking_blocks: thinkingBlocks,
808
+ signature_present: signaturePresent,
809
+ stop_reason: native.stop_reason,
810
+ usage: native.usage,
811
+ native
812
+ }
813
+ };
814
+ }
815
+
672
816
  // src/tools/chat.ts
673
817
  function estimateChatCost(mode, model, routing, routingProfile) {
674
818
  if (mode === "free") return 0;
@@ -716,14 +860,24 @@ Run blockrun_models to see all 41+ models with pricing.`,
716
860
  temperature: z2.number().optional().default(1).describe("Creativity 0-2"),
717
861
  response_format: z2.enum(["text", "json_object"]).optional().describe("Set to 'json_object' to force valid JSON output (no markdown fences). Works across all providers."),
718
862
  stop: z2.array(z2.string()).max(4).optional().describe("Up to 4 stop sequences; generation halts when any is produced"),
863
+ thinking: z2.object({
864
+ type: z2.literal("enabled"),
865
+ budget_tokens: z2.number().min(1).describe("Tokens Claude may spend reasoning before answering. max_tokens is auto-raised above this if needed.")
866
+ }).optional().describe("Anthropic extended thinking. Only honored for anthropic/claude-* models \u2014 these go direct to the native /v1/messages endpoint and the response includes verbatim type:'thinking' blocks with their original signature. Ignored for non-Claude models (no native thinking channel)."),
719
867
  agent_id: z2.string().optional().describe("Agent identifier. If a budget was delegated for this agent_id via blockrun_wallet action:'delegate', spending is tracked and enforced. The agent is hard-stopped when its budget is exhausted."),
720
868
  messages: z2.array(z2.object({
721
869
  role: z2.enum(["user", "assistant", "system"]),
722
- content: z2.string()
870
+ content: z2.union([
871
+ z2.string(),
872
+ z2.array(z2.union([
873
+ z2.object({ type: z2.literal("text"), text: z2.string() }),
874
+ z2.object({ type: z2.literal("image_url"), image_url: z2.object({ url: z2.string().describe("https URL or data:<mime>;base64,<...> URI") }) })
875
+ ]))
876
+ ]).describe("Plain text, or an array of parts for multimodal input (text + image_url). Images are honored on the native anthropic/claude-* path.")
723
877
  })).optional().describe("Conversation history for multi-turn context. When provided, 'message' is appended as the final user turn. Use with explicit 'model' param (defaults to 'openai/gpt-5.4' if not specified). Note: if you include a role:'system' entry in messages[], do not also pass the system param to avoid duplicate system messages.")
724
878
  }
725
879
  },
726
- async ({ message, model, mode, routing, routing_profile, system, max_tokens, temperature, response_format, stop, agent_id, messages }) => {
880
+ async ({ message, model, mode, routing, routing_profile, system, max_tokens, temperature, response_format, stop, thinking, agent_id, messages }) => {
727
881
  const llm = getClient();
728
882
  const responseFormat = response_format ? { type: response_format } : void 0;
729
883
  const estimatedCost = estimateChatCost(mode, model, routing, routing_profile);
@@ -734,6 +888,26 @@ Run blockrun_models to see all 41+ models with pricing.`,
734
888
  isError: true
735
889
  };
736
890
  }
891
+ if (model && isAnthropicModel(model)) {
892
+ const solanaBlock = baseOnlyMessage("Native Anthropic (claude-*) calls");
893
+ if (solanaBlock) {
894
+ return { content: [{ type: "text", text: solanaBlock }], isError: true };
895
+ }
896
+ return handleAnthropicNative({
897
+ client: getAnthropicClient(),
898
+ model,
899
+ message,
900
+ system,
901
+ messages,
902
+ maxTokens: max_tokens,
903
+ temperature,
904
+ stop,
905
+ thinking,
906
+ budget,
907
+ agentId: agent_id,
908
+ estimatedCost
909
+ });
910
+ }
737
911
  if (routing === "smart") {
738
912
  if (!(llm instanceof LLMClient2)) {
739
913
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/mcp",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "mcpName": "io.github.BlockRunAI/blockrun-mcp",
5
5
  "description": "BlockRun MCP Server - Give your AI agent web search, deep research, prediction markets, crypto data, X/Twitter intelligence. Paid via x402 micropayments.",
6
6
  "type": "module",
@@ -44,6 +44,7 @@
44
44
  "url": "https://github.com/blockrunai/blockrun-mcp/issues"
45
45
  },
46
46
  "dependencies": {
47
+ "@anthropic-ai/sdk": "^0.39.0",
47
48
  "@blockrun/llm": "^2.11.0",
48
49
  "@modelcontextprotocol/sdk": "^1.0.0",
49
50
  "open": "^11.0.0",