Thoughts, Dynamics 365, random business ideas, etc.

Adding an MCP server to Claude Desktop

Claude Desktop is really good at file-based operations. Reading documents, writing documents, filling out spreadsheets, getting context from files – sort of whatever you ask it to do. Every once in a while, though, I need it to actually DO something in an external system and there isn’t a standard connector.

Here’s what I did instead. I had Claude Code build a lightweight MCP server for me on top of an API that didn’t have one yet.

The API I picked (tariffs – yay)

I grabbed a public API that returns harmonized tariff codes and duty rates (perfect for my kitchenware company that imports parts from overseas).

What I gave Claude Code

I had Claude write the prompt.

give me a prompt to enter in claude code to build a lightweight mcp server around this (https://www.usitc.gov/documents/hts/hts_external_user_guide.pdf) using fastmcp with the intention of being locally hosted.

This spat out a pretty detailed spec for an MCP server with 3 tools – classify product, get duty rate, and compare classifications.

Build a lightweight, locally-hosted MCP server in Python using FastMCP that wraps the USITC Harmonized Tariff Schedule (HTS) REST API. This is a demo/example server for a blog post aimed at a product engineer who manages bills of materials at a kitchenware company (fictional company: Northwind Kitchenware Co. — outsources manufacturing to Vietnam and China, deals with tariff/freight cost pressure on cookware, kettles, and prep tools).

API details:
- Base URL: https://hts.usitc.gov/reststop
- No API key or auth required
- Search endpoint: GET /search?keyword={term} — returns a JSON array of tariff articles. IMPORTANT: this endpoint does broad OR keyword matching across the entire 99-chapter tariff schedule, so a search like "stainless steel kettle" returns a lot of irrelevant noise (steel slag, diamond blades, etc.) mixed in with real matches. The server needs to add real value on top of this, not just pass results through.
- Each result object has fields including: htsno, description, indent, general (MFN duty rate), special (preferential program rates), other (column 2 / non-MFN rate), units
- Some result rows are heading-level with null/empty rate fields (parent categories) rather than actual dutiable line items — filter these out when returning "real" rates.

Build these tools:

1. `classify_product(description: str, limit: int = 10)` — searches HTS codes for a product description, but narrows and ranks results to be actually useful for kitchenware:
   - Restrict/prioritize results to the chapters relevant to kitchenware and small appliances: Chapter 73 (iron/steel articles), 82 (tools/cutlery), 83 (misc base metal articles), 39 (plastics), 70 (glass), 85 (electromechanical, for things like the smart kettle line)
   - Rank by relevance to the full input phrase, not just presence of any one word — deprioritize hits that only match a single generic word (like "steel") in an unrelated context
   - Drop heading-only rows with no actual duty rate
   - Return htsno, description, general rate, and special rate for the top matches

2. `get_duty_rate(htsno: str)` — look up a specific HTS number and return a clean summary: general (MFN) rate, special program rate(s) and which trade programs they apply to, and the column 2 (non-MFN / e.g. potentially relevant for China-sourced goods) rate. Handle both fully-specified 10-digit codes and shorter codes gracefully (explain if the code is a heading, not a dutiable line item).

3. `compare_classifications(htsnos: list[str])` — takes 2-4 HTS codes and returns them side by side (description + general/special/other rates), so someone can compare the cost impact of classifying a product one way vs. another. Useful when a design choice (e.g., material, a component) could plausibly tip a product into a different, cheaper HTS heading.

Requirements:
- Use the `fastmcp` Python package (FastMCP 2.x), stdio transport, intended to run locally and be added to Claude Code / Claude Desktop's MCP config
- Single-purpose, minimal dependencies (fastmcp + httpx or requests)
- Add a simple in-memory cache (e.g. functools.lru_cache or a dict with TTL) to avoid hammering the free government API on repeated lookups
- Handle network errors / empty results gracefully with clear messages back to the model, not raw stack traces
- Include a requirements.txt and a short README.md with: what the server does, setup instructions (venv, pip install), and the exact JSON snippet to add to a Claude Code/Desktop MCP config to run it locally
- Add a few inline code comments noting where the "smart filtering" logic lives, since that's the interesting part for a blog post walkthrough

Keep the whole thing to one main server file if reasonably possible (e.g. server.py) plus README and requirements.txt — this needs to be simple enough to walk through in a blog post.

20 minutes later, I had the server

Flipped on auto mode and went to do something else. 20 minutes later, the whole thing was built. (sidenote – I’ve done this before and it was faster, but this time I used auto mode. I read somewhere recently that auto mode and thinking mode can cause Claude code to take longer, but whatever).

Making it work in Claude Desktop

Once you’ve got a locally hosted MCP server, it’s pretty easy to wire it into Claude Desktop. Settings–>Developer–>Edit Config pulls up a JSON file where you can list your MCP server. I asked Claude Code to spit that out at the end of the build – this is what it looked like:

"hts-tariff": {
      "command": "C:\\XXX\\hts-mcp-server\\.venv\\Scripts\\python.exe",
      "args": ["C:\\XXX\\hts-mcp-server\\server.py"]
    }

Close it and reopen it to see if it works

You have to close and reopen Claude Desktop for it to actually read the new config and connect. If something’s wrong with your connection string or the MCP server, this is where you’ll find out. No problems for me, though, so I popped right back into Claude Desktop to try it out.

How it worked

I ran a quick test.

classify this component and tell me the duty rate - mmw-ss304-b101.

Claude interpreted my request, realized there was a new tool to use, and gave me this response.

## Component: MMW-SS304-B101

Per the BOM (`BOM-BRW-101-Rev3.xlsx`), this is Mekong Metal Works' **304 Stainless Steel Kettle Body – Gooseneck Profile (0.9L)** — the bare stainless housing/shell for the Northwind BRW-101 Electric Gooseneck Kettle, brushed matte finish, 0.6mm wall thickness. As sourced, it's a standalone metal shell only: no heating element, cord, base, or controls attached.

**Classification: HTS 8516.90.50.00** — "Electric instantaneous or storage water heaters... other electrothermic appliances of a kind used for domestic purposes... parts thereof: Parts — Other"

**Reasoning:** Because the part ships without any electrothermic components, it lacks the essential character of a complete electric kettle (GRI 2(a) doesn't apply), and it's not one of the specifically enumerated part types under 8516.90 (not a flatiron housing, hand-dryer housing, printed-circuit assembly, or cooking-chamber assembly). Under Section XVI Note 2(b), a part suited solely for use with a heading-8516 appliance is classified as a part of that heading — landing it in the residual "Other" parts line.

**Duty rate:**
- General (MFN): **Free (0%)**
- Special programs: none listed (already duty-free MFN, so no preferential program needed)
- Column 2 (non-MFN, Cuba/North Korea only): 35%
- Note: this base HTS schedule doesn't reflect Section 301 China tariffs — since Mekong Metal Works ships from Ho Chi Minh City, Vietnam (not China), Section 301 wouldn't apply here anyway, but worth flagging as a general caveat.

For comparison, if this were instead entered as a finished stove-top (non-electric) stainless teakettle, it would fall under **7323.93.00.15** at 2% MFN (Free under most FTAs) — but that's not the right call here since it's an unassembled part for an electric appliance, not a complete non-electric kettle.

I also noted this part number and its classification context in `institutional-knowledge.md` isn't yet tracking HTS codes — let me know if you'd like me to add an HTS classification reference section there for consistency across future blog content.

Pretty slick, huh? To be clear, this is local and it’s a test. Actually shipping something like this to a team is a bigger lift than what I did. If you’ve got an API sitting around with no MCP server, though, and you want to use it with Claude desktop, go for it!


Working through something like this yourself? I answer every message personally – send me a note and I’ll get back to you.