Skip to content
Open
5 changes: 5 additions & 0 deletions .changeset/fix-nft-resource-owner.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sei-js/mcp-server': patch
---

Return the current ERC-721 owner from NFT detail resources.
6 changes: 1 addition & 5 deletions packages/mcp-server/src/core/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -545,13 +545,9 @@ export function registerEVMResources(server: McpServer) {

const nftInfo = await services.getERC721TokenMetadata(tokenAddress, tokenId, network);

// Get owner separately
let owner = 'Unknown';
try {
const isOwner = await services.isNFTOwner(tokenAddress, params.address as Address, tokenId, network);
if (isOwner) {
owner = params.address as string;
}
owner = await services.getERC721Owner(tokenAddress, tokenId, network);
} catch (_e) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] This bare catch (_e) is newly load-bearing. Before this PR the inner call could only fail one way — the template evm://{network}/nft/{tokenAddress}/{tokenId} has no {address} param, so params.address was always undefined and validateAddress threw every time, making owner unconditionally "Unknown". Good catch fixing that.

Now that a live ownerOf call happens here, the swallow collapses three distinct outcomes into the same "Unknown" string handed to the model: the token doesn't exist / was burned, the contract isn't ERC-721, and a transient RPC failure. An LLM consumer can't tell "this NFT has no owner" from "the RPC was down", and may assert the former.

Consider surfacing the reason, e.g. owner: 'Unknown' plus an ownerError: e instanceof Error ? e.message : String(e) field, so the caller has something to reason about. Same applies to the mirrored block in tools.ts get_nft_info.

// Owner info not available
}
Expand Down
35 changes: 26 additions & 9 deletions packages/mcp-server/src/core/services/balance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ const erc1155Abi = [
}
] as const;

async function readERC721Owner(tokenAddress: Address, tokenId: bigint, network: string): Promise<Address> {
return (await readContract(
{
address: tokenAddress,
abi: erc721Abi,
functionName: 'ownerOf',
args: [tokenId]
},
network
)) as Address;
}

/**
* Get the Sei balance for an address
* @param address Sei address
Expand Down Expand Up @@ -121,6 +133,19 @@ export async function getERC20Balance(
};
}

/**
* Get the current owner of a specific NFT
* @param tokenAddress NFT contract address
* @param tokenId Token ID to query
* @param network Network name or chain ID
* @returns Current owner address
*/
export async function getERC721Owner(tokenAddress: string, tokenId: bigint, network = DEFAULT_NETWORK): Promise<Address> {
const validatedTokenAddress = services.helpers.validateAddress(tokenAddress);

return readERC721Owner(validatedTokenAddress, tokenId, network);
}

/**
* Check if an address owns a specific NFT
* @param tokenAddress NFT contract address
Expand All @@ -134,15 +159,7 @@ export async function isNFTOwner(tokenAddress: string, ownerAddress: string, tok
const validatedOwnerAddress = services.helpers.validateAddress(ownerAddress);

try {
const actualOwner = (await readContract(
{
address: validatedTokenAddress,
abi: erc721Abi,
functionName: 'ownerOf',
args: [tokenId]
},
network
)) as Address;
const actualOwner = await readERC721Owner(validatedTokenAddress, tokenId, network);

return actualOwner.toLowerCase() === validatedOwnerAddress.toLowerCase();
} catch (error: unknown) {
Expand Down
20 changes: 3 additions & 17 deletions packages/mcp-server/src/core/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1038,26 +1038,12 @@ function registerWalletTools(server: McpServer) {
},
async ({ tokenAddress, tokenId, network = DEFAULT_NETWORK }) => {
try {
const nftInfo = await services.getERC721TokenMetadata(tokenAddress as Address, BigInt(tokenId), network);
const parsedTokenId = BigInt(tokenId);
const nftInfo = await services.getERC721TokenMetadata(tokenAddress as Address, parsedTokenId, network);

// Check ownership separately
let owner: `0x${string}` | null = null;
try {
// This may fail if tokenId doesn't exist
owner = await services.getPublicClient(network).readContract({
address: tokenAddress as Address,
abi: [
{
inputs: [{ type: 'uint256' }],
name: 'ownerOf',
outputs: [{ type: 'address' }],
stateMutability: 'view',
type: 'function'
}
],
functionName: 'ownerOf',
args: [BigInt(tokenId)]
});
owner = await services.getERC721Owner(tokenAddress, parsedTokenId, network);
} catch (_e) {
// Ownership info not available
}
Expand Down
Loading
Loading