Skip to content

Commit b147e49

Browse files
authored
Merge pull request #20 from nearai/contract_searching_and_build
Contract searching and build
2 parents a15163c + b6d6af4 commit b147e49

5 files changed

Lines changed: 137 additions & 103 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# @nearai/near-mcp
22

3+
## 0.0.32
4+
5+
### Patch Changes
6+
7+
- Reduce build size and improve contract search
8+
39
## 0.0.31
410

511
### Patch Changes

bun.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@nearai/near-mcp",
3-
"version": "0.0.31",
3+
"version": "0.0.32",
44
"description": "MCP server for interacting with Near Blockchain",
55
"homepage": "https://github.com/near-ai/near-mcp",
66
"repository": {
@@ -13,7 +13,7 @@
1313
"lint": "eslint . --ext .ts,.tsx",
1414
"lint:fix": "eslint . --ext .ts,.tsx --fix",
1515
"typecheck": "tsc --noEmit",
16-
"build": "rm -rf dist/* && bun build --sourcemap=external --entrypoints $(find ./src -name '*.ts' | grep -v '*.test.ts') --target node --outdir dist/ && bun run build:declaration",
16+
"build": "rm -rf dist/* && bun build --sourcemap=external --entrypoints $(find ./src -name '*.ts' | grep -v '*.test.ts') --minify --splitting --target node --outdir dist/ && bun run build:declaration",
1717
"build:declaration": "tsc --emitDeclarationOnly",
1818
"cli": "bun run build && dist/near-mcp.js"
1919
},
@@ -59,6 +59,7 @@
5959
"eslint": "^9.17.0",
6060
"eslint-config-prettier": "^9.1.0",
6161
"eslint-plugin-simple-import-sort": "^10.0.0",
62+
"openapi-types": "^12.1.3",
6263
"prettier": "^3.4.2",
6364
"shx": "^0.4.0",
6465
"typescript": "^5.7.2"

src/services.ts

Lines changed: 86 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,16 @@ import { ZSTDDecoder } from 'zstddec';
3131
import {
3232
curvePrefixToKeyType,
3333
DEFAULT_GAS,
34+
getFungibleTokenContractInfo,
3435
getParsedContractMethod,
3536
json_to_zod,
3637
keyTypeToCurvePrefix,
38+
mapSemaphore,
3739
MCP_SERVER_NAME,
3840
NearToken,
3941
noLeadingWhitespace,
4042
type Result,
41-
searchPopularFungibleTokenContractInfos,
43+
searchFungibleTokenContracts,
4244
stringify_bigint,
4345
} from './utils';
4446

@@ -576,31 +578,81 @@ export const createMcpServer = async (keyDir: string) => {
576578
);
577579

578580
mcp.tool(
579-
'system_search_popular_fungible_token_contracts',
581+
'system_search_fungible_token_contracts_info',
580582
noLeadingWhitespace`
581-
Search for popular fungible token contract information on the NEAR blockchain, with a grep-like search.
582-
Use this tool to search for popular fungible token contract information. This tool works by 'grepping'
583-
through a list of contract information JSON objects. Useful for getting contract information about popular
584-
tokens like USDC native, USDT, WNEAR, and more.`,
583+
Search for fungible token contract information on the NEAR blockchain, with a grep-like search.
584+
Use this tool to search for fungible token contract information. This tool works by 'grepping'
585+
through a list of contract information JSON objects. Be careful with this tool, it can return a lot of results.
586+
If used too much, it could overwhelm the API and cause issues.`,
585587
{
586-
searchPattern: z
588+
searchTerm: z
587589
.string()
588590
.describe(
589-
'The grep search pattern to use for filtering popular fungible token contract information.',
591+
'The search term to use for finding fungible token contract information.',
592+
),
593+
maxNumberOfResults: z
594+
.number()
595+
.default(3)
596+
.describe(
597+
'The maximum number of results to return. This is a limit to the number of results returned by the API. Keep this number low to avoid overwhelming the API.',
590598
),
591599
},
592600
async (args, __) => {
593-
const tokenContracts = await searchPopularFungibleTokenContractInfos(
594-
args.searchPattern,
601+
const tokenContractsSearchResult = await searchFungibleTokenContracts(
602+
args.searchTerm,
603+
args.maxNumberOfResults,
595604
);
596-
if (!tokenContracts.ok) {
605+
if (!tokenContractsSearchResult.ok) {
597606
return {
598-
content: [{ type: 'text', text: `Error: ${tokenContracts.error}` }],
607+
content: [
608+
{
609+
type: 'text',
610+
text: `Error: ${tokenContractsSearchResult.error}`,
611+
},
612+
],
599613
};
600614
}
615+
const tokenContracts = tokenContractsSearchResult.value;
616+
617+
// get the contract info for each contract
618+
const contractInfoResults = await mapSemaphore(
619+
tokenContracts.map((token) => token.contract),
620+
4,
621+
async (contract): Promise<[string, Result<object, Error>]> => {
622+
return [contract, await getFungibleTokenContractInfo(contract)];
623+
},
624+
);
625+
const filteredErrorResults = contractInfoResults.filter(
626+
([_, result]) => !result.ok,
627+
);
628+
if (filteredErrorResults.length > 0) {
629+
const errorTokens = filteredErrorResults
630+
.map(([contract, _]) => contract)
631+
.join(', ');
632+
return {
633+
content: [
634+
{
635+
type: 'text',
636+
text: `Error: ${errorTokens}`,
637+
},
638+
],
639+
};
640+
}
641+
const contractInfos = contractInfoResults
642+
.filter(([_, result]) => result.ok)
643+
.map(([_, result]) => {
644+
if (result.ok) {
645+
return result.value;
646+
}
647+
throw new Error('Unexpected error in filtered results');
648+
});
649+
601650
return {
602651
content: [
603-
{ type: 'text', text: stringify_bigint(tokenContracts.value) },
652+
{
653+
type: 'text',
654+
text: stringify_bigint(contractInfos),
655+
},
604656
],
605657
};
606658
},
@@ -1754,6 +1806,27 @@ export const createMcpServer = async (keyDir: string) => {
17541806
args: z
17551807
.record(z.string(), z.any())
17561808
.describe('The arguments to pass to the method.'),
1809+
jsonQuery: z
1810+
.string()
1811+
.optional()
1812+
.describe('The json query to post process.'),
1813+
outputFile: z
1814+
.string()
1815+
.optional()
1816+
.describe('The file to write the output to.'),
1817+
chainingCall: z
1818+
.object({
1819+
nextMCPServer: z
1820+
.string()
1821+
.optional()
1822+
.describe('The next MCP server to call.'),
1823+
args: z
1824+
.any()
1825+
.optional()
1826+
.describe('The arguments to pass to the next MCP server.'),
1827+
})
1828+
.optional()
1829+
.describe('The contract to call after the current contract.'),
17571830
},
17581831
async (args, _) => {
17591832
const connection = await connect({

src/utils.ts

Lines changed: 39 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { KeyType } from '@near-js/crypto';
22
import { DEFAULT_FUNCTION_CALL_GAS } from '@near-js/utils';
3+
import { type OpenAPIV3 } from 'openapi-types';
34
import { z } from 'zod';
45

56
export const DEFAULT_GAS = DEFAULT_FUNCTION_CALL_GAS * BigInt(10);
@@ -62,25 +63,21 @@ type FungibleTokenContract = {
6263
symbol: string;
6364
};
6465

65-
export const getPopularFungibleTokenContracts = async (): Promise<
66-
Result<FungibleTokenContract[], Error>
67-
> => {
66+
export const searchFungibleTokenContracts = async (
67+
searchTerm: string,
68+
maxNumberOfResults: number,
69+
): Promise<Result<FungibleTokenContract[], Error>> => {
6870
try {
69-
const url =
70-
'https://nearblocks.io/_next/data/nearblocks/en/tokens.json?page=1';
71+
const url = `https://api.nearblocks.io/v1/fts?page=1&per_page=${maxNumberOfResults}&search=${searchTerm}`;
7172
const response = await fetch(url);
7273
const data = (await response.json()) as {
73-
pageProps?: {
74-
data?: {
75-
tokens?: {
76-
contract: string;
77-
name: string;
78-
symbol: string;
79-
}[];
80-
};
81-
};
74+
tokens?: {
75+
contract: string;
76+
name: string;
77+
symbol: string;
78+
}[];
8279
};
83-
const tokens = data.pageProps?.data?.tokens;
80+
const tokens = data.tokens;
8481
if (!tokens) {
8582
return {
8683
ok: false,
@@ -103,34 +100,29 @@ export const getPopularFungibleTokenContracts = async (): Promise<
103100
};
104101

105102
export const getFungibleTokenContractInfo = async (
106-
accountId: string,
103+
contractId: string,
107104
): Promise<Result<object, Error>> => {
108105
try {
109-
const url = `https://nearblocks.io/_next/data/nearblocks/en/token/${accountId}.json`;
106+
const url = `https://api.nearblocks.io/v1/fts/${contractId}`;
110107
const response = await fetch(url);
111108
const data = (await response.json()) as {
112-
pageProps?: {
113-
tokenDetails?: {
114-
contracts?: {
115-
contract: string;
116-
name: string;
117-
symbol: string;
118-
decimals: number;
119-
description: string;
120-
website: string;
121-
}[];
122-
};
123-
};
109+
contracts?: {
110+
contract: string;
111+
name: string;
112+
symbol: string;
113+
decimals: number;
114+
description: string;
115+
website: string;
116+
}[];
124117
};
125-
const pageProps = data?.pageProps;
126-
const tokenDetails = pageProps?.tokenDetails;
127-
if (!tokenDetails?.contracts) {
118+
const contracts = data?.contracts;
119+
if (!contracts) {
128120
return {
129121
ok: false,
130122
error: new Error('No fungible token contracts found'),
131123
};
132124
}
133-
const contractInfo = tokenDetails.contracts.map((contract) => ({
125+
const contractInfo = contracts.map((contract) => ({
134126
contract: contract.contract,
135127
name: contract.name,
136128
symbol: contract.symbol,
@@ -147,61 +139,6 @@ export const getFungibleTokenContractInfo = async (
147139
}
148140
};
149141

150-
export const getPopularFungibleTokenContractInfos = async (): Promise<
151-
Result<object[], Error>
152-
> => {
153-
const popularFungibleTokenContracts =
154-
await getPopularFungibleTokenContracts();
155-
if (!popularFungibleTokenContracts.ok) {
156-
return { ok: false, error: popularFungibleTokenContracts.error };
157-
}
158-
const results = await mapSemaphore(
159-
popularFungibleTokenContracts.value.map((token) => token.contract),
160-
8,
161-
async (contract): Promise<[string, Result<object, Error>]> => {
162-
return [contract, await getFungibleTokenContractInfo(contract)];
163-
},
164-
);
165-
166-
const filteredErrorResults = results.filter(([_, result]) => !result.ok);
167-
if (filteredErrorResults.length > 0) {
168-
const errorTokens = filteredErrorResults
169-
.map(([contract, _]) => contract)
170-
.join(', ');
171-
return {
172-
ok: false,
173-
error: new Error(
174-
`Failure to receive fungible token contract info for: ${errorTokens}`,
175-
),
176-
};
177-
}
178-
const values = results
179-
.map(([_, result]) => result)
180-
.filter((result) => result.ok)
181-
.map((result) => result.value);
182-
return { ok: true, value: values };
183-
};
184-
185-
export const searchPopularFungibleTokenContractInfos = async (
186-
searchTerm: string,
187-
): Promise<Result<object[], Error>> => {
188-
const popularFungibleTokenContractInfos =
189-
await getPopularFungibleTokenContractInfos();
190-
if (!popularFungibleTokenContractInfos.ok) {
191-
return { ok: false, error: popularFungibleTokenContractInfos.error };
192-
}
193-
194-
try {
195-
const searchRegex = new RegExp(searchTerm, 'i');
196-
const filteredTokens = popularFungibleTokenContractInfos.value.filter(
197-
(token) => searchRegex.test(JSON.stringify(token)),
198-
);
199-
return { ok: true, value: filteredTokens };
200-
} catch (error) {
201-
return { ok: false, error: error as Error };
202-
}
203-
};
204-
205142
const ParsedMethodSchema = z.object({
206143
action: z.array(
207144
z.object({
@@ -252,6 +189,20 @@ export const getParsedContractMethod = async (
252189
}
253190
};
254191

192+
export const getNearBlocksJsonSchema = async (): Promise<
193+
Result<OpenAPIV3.Document, Error>
194+
> => {
195+
const url = 'https://api.nearblocks.io/openapi.json';
196+
const response = await fetch(url);
197+
const nearblocksJsonSchema = (await response.json()) as OpenAPIV3.Document;
198+
nearblocksJsonSchema.paths = Object.fromEntries(
199+
Object.entries(nearblocksJsonSchema.paths || {}).filter(([path]) =>
200+
path.includes('/v1/account/'),
201+
),
202+
);
203+
return { ok: true, value: nearblocksJsonSchema };
204+
};
205+
255206
export interface JsonToZodConfig {
256207
convertTuples?: boolean;
257208
zodValueOverrides?: Record<string, Record<string, z.ZodTypeAny>>;

0 commit comments

Comments
 (0)