#
tokens: 12094/50000 13/13 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .env.example
├── .gitignore
├── build
│   ├── api
│   │   ├── getBeerInfo.js
│   │   ├── getBeerSearch.js
│   │   └── getBreweryInfo.js
│   ├── config.js
│   ├── constants.js
│   ├── index.js
│   ├── libs
│   │   ├── format.js
│   │   └── guards.js
│   └── types
│       └── untappedApi.js
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── src
│   ├── api
│   │   ├── getBeerInfo.ts
│   │   ├── getBeerSearch.ts
│   │   └── getBreweryInfo.ts
│   ├── constants.ts
│   ├── index.ts
│   ├── libs
│   │   ├── format.ts
│   │   └── guards.ts
│   └── types
│       └── untappedApi.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------

```
1 | # Untapped API
2 | # Rename this file to .env after you add your values
3 | UNTAPPED_API_CLIENT_ID=
4 | UNTAPPED_API_CLIENT_SECRET=
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
  1 | # Logs
  2 | logs
  3 | *.log
  4 | npm-debug.log*
  5 | yarn-debug.log*
  6 | yarn-error.log*
  7 | lerna-debug.log*
  8 | .pnpm-debug.log*
  9 | 
 10 | # Diagnostic reports (https://nodejs.org/api/report.html)
 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
 12 | 
 13 | # Runtime data
 14 | pids
 15 | *.pid
 16 | *.seed
 17 | *.pid.lock
 18 | 
 19 | # Directory for instrumented libs generated by jscoverage/JSCover
 20 | lib-cov
 21 | 
 22 | # Coverage directory used by tools like istanbul
 23 | coverage
 24 | *.lcov
 25 | 
 26 | # nyc test coverage
 27 | .nyc_output
 28 | 
 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
 30 | .grunt
 31 | 
 32 | # Bower dependency directory (https://bower.io/)
 33 | bower_components
 34 | 
 35 | # node-waf configuration
 36 | .lock-wscript
 37 | 
 38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
 39 | build/Release
 40 | 
 41 | # Dependency directories
 42 | node_modules/
 43 | jspm_packages/
 44 | 
 45 | # Snowpack dependency directory (https://snowpack.dev/)
 46 | web_modules/
 47 | 
 48 | # TypeScript cache
 49 | *.tsbuildinfo
 50 | 
 51 | # Optional npm cache directory
 52 | .npm
 53 | 
 54 | # Optional eslint cache
 55 | .eslintcache
 56 | 
 57 | # Optional stylelint cache
 58 | .stylelintcache
 59 | 
 60 | # Microbundle cache
 61 | .rpt2_cache/
 62 | .rts2_cache_cjs/
 63 | .rts2_cache_es/
 64 | .rts2_cache_umd/
 65 | 
 66 | # Optional REPL history
 67 | .node_repl_history
 68 | 
 69 | # Output of 'npm pack'
 70 | *.tgz
 71 | 
 72 | # Yarn Integrity file
 73 | .yarn-integrity
 74 | 
 75 | # dotenv environment variable files
 76 | .env
 77 | .env.development.local
 78 | .env.test.local
 79 | .env.production.local
 80 | .env.local
 81 | 
 82 | # parcel-bundler cache (https://parceljs.org/)
 83 | .cache
 84 | .parcel-cache
 85 | 
 86 | # Next.js build output
 87 | .next
 88 | out
 89 | 
 90 | # Nuxt.js build / generate output
 91 | .nuxt
 92 | dist
 93 | 
 94 | # Gatsby files
 95 | .cache/
 96 | # Comment in the public line in if your project uses Gatsby and not Next.js
 97 | # https://nextjs.org/blog/next-9-1#public-directory-support
 98 | # public
 99 | 
100 | # vuepress build output
101 | .vuepress/dist
102 | 
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 | 
107 | # Docusaurus cache and generated files
108 | .docusaurus
109 | 
110 | # Serverless directories
111 | .serverless/
112 | 
113 | # FuseBox cache
114 | .fusebox/
115 | 
116 | # DynamoDB Local files
117 | .dynamodb/
118 | 
119 | # TernJS port file
120 | .tern-port
121 | 
122 | # Stores VSCode versions used for testing VSCode extensions
123 | .vscode-test
124 | 
125 | # yarn v2
126 | .yarn/cache
127 | .yarn/unplugged
128 | .yarn/build-state.yml
129 | .yarn/install-state.gz
130 | .pnp.*
131 | 
132 | # idea
133 | .idea
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
 1 | # untapped-mcp
 2 | 
 3 | A Untapped MCP server to be used with claude.
 4 | 
 5 | ## Setup
 6 | 
 7 | ### Get API Key
 8 | 
 9 | ### Usage with Claude Desktop
10 | 
11 | Add the following to your `claude_desktop_config.json`:
12 | 
13 | ```json
14 | {
15 |   "mcpServers": {
16 |     "Untappd": {
17 |       "command": "node",
18 |       "args": ["/Users/user/projects/untapped-mcp/build/index.js"],
19 |       "env": {
20 |         "UNTAPPED_API_CLIENT_ID": "<YOUR_CLIENT_ID>",
21 |         "UNTAPPED_API_CLIENT_SECRET": "<YOUR_CLIENT_SECRET>"
22 |       }
23 |     }
24 |   }
25 | }
26 | ```
27 | 
```

--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------

```typescript
1 | // Untapped
2 | export const UNTAPPED_API_BASE = "https://api.untappd.com/v4/";
3 | export const UNTAPPED_API_SEARCH = "search/beer";
4 | export const UNTAPPED_API_INFO = "beer/info";
5 | export const UNTAPPED_API_BREWERY_INFO = "/brewery/info/";
6 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "Node16",
 5 |     "moduleResolution": "Node16",
 6 |     "outDir": "./build",
 7 |     "rootDir": "./src",
 8 |     "strict": true,
 9 |     "esModuleInterop": true,
10 |     "skipLibCheck": true,
11 |     "forceConsistentCasingInFileNames": true
12 |   },
13 |   "include": ["src/**/*"],
14 |   "exclude": ["node_modules"]
15 | }
16 | 
```

--------------------------------------------------------------------------------
/src/libs/guards.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { UntappdApiErrorResponse } from "../types/untappedApi.js";
 2 | 
 3 | export function isUntappdApiError(
 4 |   value: unknown,
 5 | ): value is UntappdApiErrorResponse {
 6 |   return (
 7 |     typeof value === "object" &&
 8 |     value !== null &&
 9 |     "meta" in value &&
10 |     typeof (value as any).meta === "object" &&
11 |     "code" in (value as any).meta &&
12 |     "error_detail" in (value as any).meta &&
13 |     "error_type" in (value as any).meta
14 |   );
15 | }
16 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "untapped-mcp",
 3 |   "version": "1.0.0",
 4 |   "description": "A Untapped MCP server to be used with claude.",
 5 |   "main": "index.js",
 6 |   "repository": {
 7 |     "type": "git",
 8 |     "url": "git+https://github.com/etoxin/untapped-mcp.git"
 9 |   },
10 |   "keywords": [],
11 |   "author": "Adam Lusted",
12 |   "license": "ISC",
13 |   "bugs": {
14 |     "url": "https://github.com/etoxin/untapped-mcp/issues"
15 |   },
16 |   "homepage": "https://github.com/etoxin/untapped-mcp#readme",
17 |   "type": "module",
18 |   "bin": {
19 |     "untapped": "./build/index.js"
20 |   },
21 |   "files": [
22 |     "build"
23 |   ],
24 |   "scripts": {
25 |     "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
26 |     "test": "echo \"Error: no test specified\" && exit 1"
27 |   },
28 |   "dependencies": {
29 |     "@modelcontextprotocol/sdk": "^1.6.1",
30 |     "axios": "^1.8.2",
31 |     "dotenv": "^16.4.7",
32 |     "zod": "^3.24.2"
33 |   },
34 |   "devDependencies": {
35 |     "@types/node": "^22.13.9",
36 |     "typescript": "^5.8.2"
37 |   }
38 | }
39 | 
```

--------------------------------------------------------------------------------
/src/api/getBeerInfo.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import axios from "axios";
 2 | import { UNTAPPED_API_BASE, UNTAPPED_API_INFO } from "../constants.js";
 3 | import { isUntappdApiError } from "../libs/guards.js";
 4 | import { UntappdBeerInfoResult } from "../types/untappedApi.js";
 5 | import { config } from "../index.js";
 6 | 
 7 | export async function GetBeerInfo(bid: string) {
 8 |   try {
 9 |     const response = await axios.get<UntappdBeerInfoResult>(
10 |       `${UNTAPPED_API_BASE}${UNTAPPED_API_INFO}/${bid}`,
11 |       {
12 |         params: {
13 |           client_id: config.untappd.clientId,
14 |           client_secret: config.untappd.clientSecret,
15 |         },
16 |       },
17 |     );
18 | 
19 |     return response.data;
20 |   } catch (e: unknown) {
21 |     if (axios.isAxiosError(e) && e.response) {
22 |       throw new Error(`HTTP error! status: ${e.response.status}`);
23 |     }
24 |     if (e instanceof Error) {
25 |       return e.message;
26 |     }
27 |     if (isUntappdApiError(e)) {
28 |       return e.meta.error_detail;
29 |     }
30 |     return "An unknown error occurred";
31 |   }
32 | }
33 | 
```

--------------------------------------------------------------------------------
/src/api/getBreweryInfo.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import axios from "axios";
 2 | import {
 3 |   UNTAPPED_API_BASE,
 4 |   UNTAPPED_API_BREWERY_INFO,
 5 |   UNTAPPED_API_INFO,
 6 | } from "../constants.js";
 7 | import { isUntappdApiError } from "../libs/guards.js";
 8 | import { UntappdBreweryInfoResult } from "../types/untappedApi.js";
 9 | import { config } from "../index.js";
10 | 
11 | export async function GetBreweryInfo(breweryId: string) {
12 |   try {
13 |     const response = await axios.get<UntappdBreweryInfoResult>(
14 |       `${UNTAPPED_API_BASE}${UNTAPPED_API_BREWERY_INFO}/${breweryId}`,
15 |       {
16 |         params: {
17 |           client_id: config.untappd.clientId,
18 |           client_secret: config.untappd.clientSecret,
19 |         },
20 |       },
21 |     );
22 | 
23 |     return response.data;
24 |   } catch (e: unknown) {
25 |     if (axios.isAxiosError(e) && e.response) {
26 |       throw new Error(`HTTP error! status: ${e.response.status}`);
27 |     }
28 |     if (e instanceof Error) {
29 |       return e.message;
30 |     }
31 |     if (isUntappdApiError(e)) {
32 |       return e.meta.error_detail;
33 |     }
34 |     return "An unknown error occurred";
35 |   }
36 | }
37 | 
```

--------------------------------------------------------------------------------
/src/api/getBeerSearch.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import axios from "axios";
 2 | import { UNTAPPED_API_SEARCH, UNTAPPED_API_BASE } from "../constants.js";
 3 | import { isUntappdApiError } from "../libs/guards.js";
 4 | import { UntappdBeerSearchResult } from "../types/untappedApi.js";
 5 | import { config } from "../index.js";
 6 | 
 7 | export async function getBeerSearch(query: string) {
 8 |   try {
 9 |     const response = await axios.get<UntappdBeerSearchResult>(
10 |       `${UNTAPPED_API_BASE}${UNTAPPED_API_SEARCH}`,
11 |       {
12 |         params: {
13 |           q: query,
14 |           client_id: config.untappd.clientId,
15 |           client_secret: config.untappd.clientSecret,
16 |         },
17 |       },
18 |     );
19 | 
20 |     return response.data;
21 |   } catch (e: unknown) {
22 |     if (axios.isAxiosError(e) && e.response) {
23 |       throw new Error(
24 |         `HTTP error! status: ${e.response.status}: ${JSON.stringify(e)}`,
25 |       );
26 |     }
27 |     if (e instanceof Error) {
28 |       return e.message;
29 |     }
30 |     if (isUntappdApiError(e)) {
31 |       return e.meta.error_detail;
32 |     }
33 |     return "An unknown error occurred";
34 |   }
35 | }
36 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
  2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  3 | import { z } from "zod";
  4 | import { getBeerSearch } from "./api/getBeerSearch.js";
  5 | import {
  6 |   formatUntappdBeerInfoResult,
  7 |   formatUntappdBeerSearchResult,
  8 |   formatUntappdBreweryInfoResult,
  9 | } from "./libs/format.js";
 10 | 
 11 | import dotenv from "dotenv";
 12 | import { GetBeerInfo } from "./api/getBeerInfo.js";
 13 | import { GetBreweryInfo } from "./api/getBreweryInfo.js";
 14 | dotenv.config();
 15 | 
 16 | export const config = {
 17 |   untappd: {
 18 |     clientId: process.env.UNTAPPED_API_CLIENT_ID,
 19 |     clientSecret: process.env.UNTAPPED_API_CLIENT_SECRET,
 20 |   },
 21 | };
 22 | 
 23 | // Create server instance
 24 | const server = new McpServer({
 25 |   name: "untapped",
 26 |   version: "1.0.0",
 27 | });
 28 | 
 29 | // Register untapped tools
 30 | server.tool(
 31 |   "Beer_Search",
 32 |   "Search beers on untapped",
 33 |   {
 34 |     beer: z.string().describe("The name of the beer you want to search"),
 35 |   },
 36 |   async ({ beer }) => {
 37 |     const beersData = await getBeerSearch(beer);
 38 | 
 39 |     if (!beersData) {
 40 |       return {
 41 |         content: [
 42 |           {
 43 |             type: "text",
 44 |             text: "Failed to retrieve untapped beer data.",
 45 |           },
 46 |         ],
 47 |       };
 48 |     }
 49 | 
 50 |     if (typeof beersData === "string") {
 51 |       return {
 52 |         content: [
 53 |           {
 54 |             type: "text",
 55 |             text: `Failed to retrieve untapped beer data: ${beersData}`,
 56 |           },
 57 |         ],
 58 |       };
 59 |     }
 60 | 
 61 |     const formattedBeerData = formatUntappdBeerSearchResult(beersData);
 62 | 
 63 |     return {
 64 |       content: [
 65 |         {
 66 |           type: "text",
 67 |           text: formattedBeerData,
 68 |         },
 69 |       ],
 70 |     };
 71 |   },
 72 | );
 73 | 
 74 | server.tool(
 75 |   "Beer_Info",
 76 |   "Get detailed info of a beer.",
 77 |   {
 78 |     bid: z
 79 |       .string()
 80 |       .describe(
 81 |         "Beer ID (string): The 'bid' can be retrieved from 'Beer Search'.",
 82 |       ),
 83 |   },
 84 |   async ({ bid }) => {
 85 |     const beerInfoData = await GetBeerInfo(bid);
 86 | 
 87 |     if (!beerInfoData) {
 88 |       return {
 89 |         content: [
 90 |           {
 91 |             type: "text",
 92 |             text: "Failed to retrieve untapped beer info data.",
 93 |           },
 94 |         ],
 95 |       };
 96 |     }
 97 | 
 98 |     if (typeof beerInfoData === "string") {
 99 |       return {
100 |         content: [
101 |           {
102 |             type: "text",
103 |             text: `Failed to retrieve untapped beer info data: ${beerInfoData}`,
104 |           },
105 |         ],
106 |       };
107 |     }
108 | 
109 |     const formattedBeerInfoData = formatUntappdBeerInfoResult(beerInfoData);
110 | 
111 |     return {
112 |       content: [
113 |         {
114 |           type: "text",
115 |           text: formattedBeerInfoData,
116 |         },
117 |       ],
118 |     };
119 |   },
120 | );
121 | 
122 | server.tool(
123 |   "Brewery_Info",
124 |   "Get detailed info of a brewery.",
125 |   {
126 |     brewery_id: z
127 |       .string()
128 |       .describe(
129 |         "brewery_id (string): The 'brewery_id' can be retrieved from a beer.",
130 |       ),
131 |   },
132 |   async ({ brewery_id }) => {
133 |     const breweryInfoData = await GetBreweryInfo(brewery_id);
134 | 
135 |     if (!breweryInfoData) {
136 |       return {
137 |         content: [
138 |           {
139 |             type: "text",
140 |             text: "Failed to retrieve untapped brewery info data.",
141 |           },
142 |         ],
143 |       };
144 |     }
145 | 
146 |     if (typeof breweryInfoData === "string") {
147 |       return {
148 |         content: [
149 |           {
150 |             type: "text",
151 |             text: `Failed to retrieve untapped brewery info data: ${breweryInfoData}`,
152 |           },
153 |         ],
154 |       };
155 |     }
156 | 
157 |     const formattedBreweryInfoData =
158 |       formatUntappdBreweryInfoResult(breweryInfoData);
159 | 
160 |     return {
161 |       content: [
162 |         {
163 |           type: "text",
164 |           text: formattedBreweryInfoData,
165 |         },
166 |       ],
167 |     };
168 |   },
169 | );
170 | 
171 | async function main() {
172 |   const transport = new StdioServerTransport();
173 |   await server.connect(transport);
174 |   console.error("Untapped MCP Server running on stdio");
175 | }
176 | 
177 | main().catch((error) => {
178 |   console.error("Fatal error in main():", error);
179 |   process.exit(1);
180 | });
181 | 
```

--------------------------------------------------------------------------------
/src/types/untappedApi.ts:
--------------------------------------------------------------------------------

```typescript
  1 | // Common types
  2 | 
  3 | // Contact information for brewery
  4 | interface UntappdContact {
  5 |   twitter?: string;
  6 |   facebook?: string;
  7 |   instagram?: string;
  8 |   url?: string;
  9 | }
 10 | 
 11 | // Location information for brewery
 12 | interface UntappdLocation {
 13 |   brewery_city?: string;
 14 |   brewery_state?: string;
 15 |   venue_address?: string;
 16 |   venue_city?: string;
 17 |   venue_state?: string;
 18 |   lat?: number;
 19 |   lng?: number;
 20 | }
 21 | 
 22 | // Brewery details
 23 | interface UntappdBrewery {
 24 |   brewery_id: number;
 25 |   brewery_name: string;
 26 |   brewery_slug?: string;
 27 |   brewery_label: string;
 28 |   country_name: string;
 29 |   contact: UntappdContact;
 30 |   location: UntappdLocation;
 31 |   brewery_active?: number;
 32 |   beer_count?: number; // Only in breweries section
 33 | }
 34 | 
 35 | // Beer details
 36 | interface UntappdBeer {
 37 |   bid: number;
 38 |   beer_name: string;
 39 |   beer_label: string;
 40 |   beer_abv: number;
 41 |   beer_ibu: number;
 42 |   beer_description: string;
 43 |   created_at: string;
 44 |   beer_style: string;
 45 |   auth_rating: number;
 46 |   wish_list: boolean;
 47 |   in_production?: number;
 48 |   beer_slug?: string;
 49 |   beer_style_id?: number;
 50 |   beer_active?: number;
 51 |   is_in_production?: number;
 52 |   is_vintage?: number;
 53 |   is_variant?: number;
 54 |   is_homebrew?: number;
 55 |   rating_count?: number;
 56 |   rating_score?: number;
 57 | }
 58 | 
 59 | // Beer stats
 60 | interface UntappdBeerStats {
 61 |   total_count: number;
 62 |   monthly_count: number;
 63 |   total_user_count: number;
 64 |   user_count: number;
 65 | }
 66 | 
 67 | // User information
 68 | interface UntappdUser {
 69 |   uid: number;
 70 |   user_name: string;
 71 |   first_name: string;
 72 |   last_name: string;
 73 |   user_avatar: string;
 74 |   relationship: string;
 75 |   is_private: number;
 76 | }
 77 | 
 78 | // Venue category
 79 | interface UntappdVenueCategory {
 80 |   category_name: string;
 81 |   category_id: string;
 82 |   is_primary: boolean;
 83 | }
 84 | 
 85 | // Venue categories section
 86 | interface UntappdVenueCategories {
 87 |   count: number;
 88 |   items: UntappdVenueCategory[];
 89 | }
 90 | 
 91 | // Foursquare venue info
 92 | interface UntappdFoursquareVenue {
 93 |   foursquare_id: string;
 94 |   foursquare_url: string;
 95 | }
 96 | 
 97 | // Venue icon
 98 | interface UntappdVenueIcon {
 99 |   sm: string;
100 |   md: string;
101 |   lg: string;
102 | }
103 | 
104 | // Venue information
105 | interface UntappdVenue {
106 |   venue_id: number;
107 |   venue_name: string;
108 |   primary_category: string;
109 |   parent_category_id: string;
110 |   categories: UntappdVenueCategories;
111 |   location: UntappdLocation;
112 |   contact: UntappdContact;
113 |   private_venue: boolean;
114 |   foursquare: UntappdFoursquareVenue;
115 |   venue_icon: UntappdVenueIcon;
116 | }
117 | 
118 | // Photo information
119 | interface UntappdPhoto {
120 |   photo_img_sm: string;
121 |   photo_img_md: string;
122 |   photo_img_lg: string;
123 |   photo_img_og: string;
124 | }
125 | 
126 | // Media item for beer
127 | export interface UntappdMediaItem {
128 |   photo_id: number;
129 |   photo: UntappdPhoto;
130 |   created_at: string;
131 |   checkin_id: number;
132 |   beer: UntappdBeer;
133 |   brewery: UntappdBrewery;
134 |   user: UntappdUser;
135 |   venue: UntappdVenue[];
136 | }
137 | 
138 | // Media section
139 | export interface UntappdMedia {
140 |   count: number;
141 |   items: UntappdMediaItem | UntappdMediaItem[]; // API inconsistently returns object or array
142 | }
143 | 
144 | // Similar beer item
145 | interface UntappdSimilarBeerItem {
146 |   rating_score: number;
147 |   beer: UntappdBeer;
148 |   brewery: UntappdBrewery;
149 |   friends: {
150 |     items: any[]; // Can be more specific if needed
151 |     count: number;
152 |   };
153 | }
154 | 
155 | // Similar beers section
156 | interface UntappdSimilarBeers {
157 |   count: number;
158 |   items: UntappdSimilarBeerItem | UntappdSimilarBeerItem[]; // API inconsistently returns object or array
159 | }
160 | 
161 | // Friends section
162 | interface UntappdFriends {
163 |   count: number;
164 |   items: any[]; // Can be more specific if needed
165 | }
166 | 
167 | // Vintage item
168 | interface UntappdVintageItem {
169 |   beer: {
170 |     bid: number;
171 |     beer_label: string;
172 |     beer_slug: string;
173 |     beer_name: string;
174 |     is_vintage: number;
175 |     is_variant: number;
176 |   };
177 | }
178 | 
179 | // Vintages section
180 | interface UntappdVintages {
181 |   count: number;
182 |   items: UntappdVintageItem[];
183 | }
184 | 
185 | // Complete beer info
186 | export interface UntappdBeerInfo extends UntappdBeer {
187 |   stats: UntappdBeerStats;
188 |   brewery: UntappdBrewery;
189 |   media: UntappdMedia;
190 |   similar: UntappdSimilarBeers;
191 |   friends: UntappdFriends;
192 |   vintages: UntappdVintages;
193 | }
194 | 
195 | // Beer item in search results
196 | export interface UntappdBeerItem {
197 |   checkin_count: number;
198 |   have_had: boolean;
199 |   your_count: number;
200 |   beer: UntappdBeer;
201 |   brewery: UntappdBrewery;
202 | }
203 | 
204 | // Brewery item in search results
205 | interface UntappdBreweryItem {
206 |   brewery: UntappdBrewery;
207 | }
208 | 
209 | // Beer section in response
210 | interface UntappdBeerSection {
211 |   count: number;
212 |   items: UntappdBeerItem[];
213 | }
214 | 
215 | // Brewery section in response
216 | interface UntappdBrewerySection {
217 |   count: number;
218 |   items: UntappdBreweryItem[];
219 | }
220 | 
221 | // Response wrapper with metadata (common to all Untappd API responses)
222 | export interface UntappdApiResponse<T> {
223 |   meta: {
224 |     code: number;
225 |     response_time: {
226 |       time: number;
227 |       measure: string;
228 |     };
229 |     error_detail?: string;
230 |     error_type?: string;
231 |     developer_friendly?: string;
232 |   };
233 |   notifications: Record<string, unknown>;
234 |   response: T;
235 | }
236 | 
237 | // Untappd API Error Response
238 | export interface UntappdApiErrorResponse {
239 |   meta: {
240 |     code: number;
241 |     error_detail: string;
242 |     error_type: string;
243 |     developer_friendly?: string;
244 |     response_time: {
245 |       time: number;
246 |       measure: string;
247 |     };
248 |   };
249 | }
250 | 
251 | // Complete beer info response
252 | export interface UntappdBeerInfoResponse {
253 |   beer: UntappdBeerInfo;
254 | }
255 | 
256 | // Complete search response
257 | export interface UntappdBeerSearchResponse {
258 |   found: number;
259 |   offset: number;
260 |   limit: number;
261 |   term: string;
262 |   parsed_term: string;
263 |   beers: UntappdBeerSection;
264 |   homebrew: UntappdBeerSection;
265 |   breweries: UntappdBrewerySection;
266 | }
267 | 
268 | // Additional Untappd types for brewery information
269 | 
270 | // Claimed status information for brewery
271 | interface UntappdClaimedStatus {
272 |   is_claimed: boolean;
273 |   claimed_slug: string;
274 |   follow_status: boolean;
275 |   follower_count: number;
276 |   uid: number;
277 |   mute_status: string;
278 | }
279 | 
280 | // Brewery rating information
281 | interface UntappdBreweryRating {
282 |   count: number;
283 |   rating_score: number;
284 | }
285 | 
286 | // Extended brewery statistics
287 | interface UntappdBreweryStats {
288 |   total_count: number;
289 |   unique_count: number;
290 |   monthly_count: number;
291 |   weekly_count: number;
292 |   user_count: number;
293 |   age_on_service: number;
294 | }
295 | 
296 | // Brewery owners section
297 | interface UntappdBreweryOwners {
298 |   count: number;
299 |   items: any[]; // Could be more specific if needed
300 | }
301 | 
302 | // Beer list item in brewery response
303 | interface UntappdBeerListItem {
304 |   has_had: boolean;
305 |   total_count: number;
306 |   beer: UntappdBeer;
307 |   brewery: UntappdBrewery;
308 |   friends: any[]; // Could be more specific if needed
309 | }
310 | 
311 | // Beer list section in brewery response
312 | interface UntappdBeerList {
313 |   is_super: boolean;
314 |   sort: string;
315 |   filter: string;
316 |   count: number;
317 |   items: UntappdBeerListItem | UntappdBeerListItem[]; // API inconsistently returns object or array
318 |   beer_count: number;
319 | }
320 | 
321 | // Extended brewery information
322 | export interface UntappdBreweryInfo extends UntappdBrewery {
323 |   brewery_in_production: number;
324 |   is_independent: number;
325 |   claimed_status: UntappdClaimedStatus;
326 |   brewery_type: string;
327 |   brewery_type_id: number;
328 |   brewery_description: string;
329 |   rating: UntappdBreweryRating;
330 |   stats: UntappdBreweryStats;
331 |   owners: UntappdBreweryOwners;
332 |   media: UntappdMedia;
333 |   beer_list: UntappdBeerList;
334 | }
335 | 
336 | // Complete brewery info response
337 | export interface UntappdBreweryInfoResponse {
338 |   brewery: UntappdBreweryInfo;
339 | }
340 | 
341 | // Complete typed response for brewery info
342 | export type UntappdBreweryInfoResult =
343 |   UntappdApiResponse<UntappdBreweryInfoResponse>;
344 | 
345 | // Complete typed response for beer search
346 | export type UntappdBeerSearchResult =
347 |   UntappdApiResponse<UntappdBeerSearchResponse>;
348 | 
349 | // Complete typed response for beer info
350 | export type UntappdBeerInfoResult = UntappdApiResponse<UntappdBeerInfoResponse>;
351 | 
```

--------------------------------------------------------------------------------
/src/libs/format.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import {
  2 |   UntappdBeerInfo,
  3 |   UntappdBeerInfoResult,
  4 |   UntappdBeerItem,
  5 |   UntappdBeerSearchResult,
  6 |   UntappdBreweryInfoResult,
  7 |   UntappdMediaItem,
  8 | } from "../types/untappedApi.js";
  9 | 
 10 | export function formatUntappdBeerItem(beerItem: UntappdBeerItem): string {
 11 |   return [
 12 |     `Basic beer item properties`,
 13 |     `---`,
 14 |     `checkin_count: ${beerItem.checkin_count}`,
 15 |     `have_had: ${beerItem.have_had}`,
 16 |     `your_count: ${beerItem.your_count}`,
 17 | 
 18 |     `Beer properties`,
 19 |     `---`,
 20 |     `bid: ${beerItem.beer.bid}`,
 21 |     `beer_name: ${beerItem.beer.beer_name}`,
 22 |     `beer_label: ${beerItem.beer.beer_label}`,
 23 |     `beer_abv: ${beerItem.beer.beer_abv}`,
 24 |     `beer_ibu: ${beerItem.beer.beer_ibu}`,
 25 |     `beer_description: ${beerItem.beer.beer_description}`,
 26 |     `created_at: ${beerItem.beer.created_at}`,
 27 |     `beer_style: ${beerItem.beer.beer_style}`,
 28 |     `auth_rating: ${beerItem.beer.auth_rating}`,
 29 |     `wish_list: ${beerItem.beer.wish_list}`,
 30 | 
 31 |     `Optional beer properties`,
 32 |     `---`,
 33 |     ...(beerItem.beer.in_production !== undefined
 34 |       ? [`in_production: ${beerItem.beer.in_production}`]
 35 |       : []),
 36 |     ...(beerItem.beer.beer_slug
 37 |       ? [`beer_slug: ${beerItem.beer.beer_slug}`]
 38 |       : []),
 39 |     ...(beerItem.beer.beer_style_id
 40 |       ? [`beer_style_id: ${beerItem.beer.beer_style_id}`]
 41 |       : []),
 42 |     ...(beerItem.beer.beer_active !== undefined
 43 |       ? [`beer_active: ${beerItem.beer.beer_active}`]
 44 |       : []),
 45 |     ...(beerItem.beer.is_in_production !== undefined
 46 |       ? [`is_in_production: ${beerItem.beer.is_in_production}`]
 47 |       : []),
 48 |     ...(beerItem.beer.is_vintage !== undefined
 49 |       ? [`is_vintage: ${beerItem.beer.is_vintage}`]
 50 |       : []),
 51 |     ...(beerItem.beer.is_variant !== undefined
 52 |       ? [`is_variant: ${beerItem.beer.is_variant}`]
 53 |       : []),
 54 |     ...(beerItem.beer.is_homebrew !== undefined
 55 |       ? [`is_homebrew: ${beerItem.beer.is_homebrew}`]
 56 |       : []),
 57 |     ...(beerItem.beer.rating_count !== undefined
 58 |       ? [`rating_count: ${beerItem.beer.rating_count}`]
 59 |       : []),
 60 |     ...(beerItem.beer.rating_score !== undefined
 61 |       ? [`rating_score: ${beerItem.beer.rating_score}`]
 62 |       : []),
 63 | 
 64 |     `Brewery properties`,
 65 |     `---`,
 66 |     `brewery_id: ${beerItem.brewery.brewery_id}`,
 67 |     `brewery_name: ${beerItem.brewery.brewery_name}`,
 68 |     `brewery_label: ${beerItem.brewery.brewery_label}`,
 69 |     `country_name: ${beerItem.brewery.country_name}`,
 70 | 
 71 |     `Optional brewery properties`,
 72 |     `---`,
 73 |     ...(beerItem.brewery.brewery_slug
 74 |       ? [`brewery_slug: ${beerItem.brewery.brewery_slug}`]
 75 |       : []),
 76 |     ...(beerItem.brewery.brewery_active !== undefined
 77 |       ? [`brewery_active: ${beerItem.brewery.brewery_active}`]
 78 |       : []),
 79 |     ...(beerItem.brewery.beer_count !== undefined
 80 |       ? [`brewery_beer_count: ${beerItem.brewery.beer_count}`]
 81 |       : []),
 82 | 
 83 |     `Brewery contact information`,
 84 |     `---`,
 85 |     ...(beerItem.brewery.contact?.twitter
 86 |       ? [`brewery_twitter: ${beerItem.brewery.contact.twitter}`]
 87 |       : []),
 88 |     ...(beerItem.brewery.contact?.facebook
 89 |       ? [`brewery_facebook: ${beerItem.brewery.contact.facebook}`]
 90 |       : []),
 91 |     ...(beerItem.brewery.contact?.instagram
 92 |       ? [`brewery_instagram: ${beerItem.brewery.contact.instagram}`]
 93 |       : []),
 94 |     ...(beerItem.brewery.contact?.url
 95 |       ? [`brewery_url: ${beerItem.brewery.contact.url}`]
 96 |       : []),
 97 | 
 98 |     `Brewery location`,
 99 |     `---`,
100 |     ...(beerItem.brewery.location?.brewery_city
101 |       ? [`brewery_city: ${beerItem.brewery.location.brewery_city}`]
102 |       : []),
103 |     ...(beerItem.brewery.location?.brewery_state
104 |       ? [`brewery_state: ${beerItem.brewery.location.brewery_state}`]
105 |       : []),
106 |     ...(beerItem.brewery.location?.venue_address
107 |       ? [`venue_address: ${beerItem.brewery.location.venue_address}`]
108 |       : []),
109 |     ...(beerItem.brewery.location?.venue_city
110 |       ? [`venue_city: ${beerItem.brewery.location.venue_city}`]
111 |       : []),
112 |     ...(beerItem.brewery.location?.venue_state
113 |       ? [`venue_state: ${beerItem.brewery.location.venue_state}`]
114 |       : []),
115 |     ...(beerItem.brewery.location?.lat !== undefined
116 |       ? [`latitude: ${beerItem.brewery.location.lat}`]
117 |       : []),
118 |     ...(beerItem.brewery.location?.lng !== undefined
119 |       ? [`longitude: ${beerItem.brewery.location.lng}`]
120 |       : []),
121 |     "+++ End",
122 |   ].join("\n");
123 | }
124 | 
125 | export function formatUntappdBeerInfo(beerInfo: UntappdBeerInfo): string {
126 |   return [
127 |     `Beer Information`,
128 |     `---`,
129 |     `bid: ${beerInfo.bid}`,
130 |     `beer_name: ${beerInfo.beer_name}`,
131 |     `beer_label: ${beerInfo.beer_label}`,
132 |     `beer_abv: ${beerInfo.beer_abv}`,
133 |     `beer_ibu: ${beerInfo.beer_ibu}`,
134 |     `beer_description: ${beerInfo.beer_description}`,
135 |     `created_at: ${beerInfo.created_at}`,
136 |     `beer_style: ${beerInfo.beer_style}`,
137 |     `auth_rating: ${beerInfo.auth_rating}`,
138 | 
139 |     `Optional Beer Properties`,
140 |     `---`,
141 |     ...(beerInfo.in_production !== undefined
142 |       ? [`in_production: ${beerInfo.in_production}`]
143 |       : []),
144 |     ...(beerInfo.beer_slug ? [`beer_slug: ${beerInfo.beer_slug}`] : []),
145 |     ...(beerInfo.beer_style_id
146 |       ? [`beer_style_id: ${beerInfo.beer_style_id}`]
147 |       : []),
148 |     ...(beerInfo.beer_active !== undefined
149 |       ? [`beer_active: ${beerInfo.beer_active}`]
150 |       : []),
151 |     ...(beerInfo.is_in_production !== undefined
152 |       ? [`is_in_production: ${beerInfo.is_in_production}`]
153 |       : []),
154 |     ...(beerInfo.is_vintage !== undefined
155 |       ? [`is_vintage: ${beerInfo.is_vintage}`]
156 |       : []),
157 |     ...(beerInfo.is_variant !== undefined
158 |       ? [`is_variant: ${beerInfo.is_variant}`]
159 |       : []),
160 |     ...(beerInfo.is_homebrew !== undefined
161 |       ? [`is_homebrew: ${beerInfo.is_homebrew}`]
162 |       : []),
163 |     ...(beerInfo.rating_count !== undefined
164 |       ? [`rating_count: ${beerInfo.rating_count}`]
165 |       : []),
166 |     ...(beerInfo.rating_score !== undefined
167 |       ? [`rating_score: ${beerInfo.rating_score}`]
168 |       : []),
169 | 
170 |     `Beer Stats`,
171 |     `---`,
172 |     `total_count: ${beerInfo.stats.total_count}`,
173 |     `monthly_count: ${beerInfo.stats.monthly_count}`,
174 |     `total_user_count: ${beerInfo.stats.total_user_count}`,
175 |     `user_count: ${beerInfo.stats.user_count}`,
176 | 
177 |     `Brewery Information`,
178 |     `---`,
179 |     `brewery_id: ${beerInfo.brewery.brewery_id}`,
180 |     `brewery_name: ${beerInfo.brewery.brewery_name}`,
181 |     `brewery_label: ${beerInfo.brewery.brewery_label}`,
182 |     `country_name: ${beerInfo.brewery.country_name}`,
183 | 
184 |     `Optional Brewery Properties`,
185 |     `---`,
186 |     ...(beerInfo.brewery.brewery_slug
187 |       ? [`brewery_slug: ${beerInfo.brewery.brewery_slug}`]
188 |       : []),
189 | 
190 |     `Brewery Location`,
191 |     `---`,
192 |     ...(beerInfo.brewery.location?.brewery_city
193 |       ? [`brewery_city: ${beerInfo.brewery.location.brewery_city}`]
194 |       : []),
195 |     ...(beerInfo.brewery.location?.brewery_state
196 |       ? [`brewery_state: ${beerInfo.brewery.location.brewery_state}`]
197 |       : []),
198 |     ...(beerInfo.brewery.location?.venue_address
199 |       ? [`venue_address: ${beerInfo.brewery.location.venue_address}`]
200 |       : []),
201 |     ...(beerInfo.brewery.location?.venue_city
202 |       ? [`venue_city: ${beerInfo.brewery.location.venue_city}`]
203 |       : []),
204 |     ...(beerInfo.brewery.location?.venue_state
205 |       ? [`venue_state: ${beerInfo.brewery.location.venue_state}`]
206 |       : []),
207 |     ...(beerInfo.brewery.location?.lat !== undefined
208 |       ? [`latitude: ${beerInfo.brewery.location.lat}`]
209 |       : []),
210 |     ...(beerInfo.brewery.location?.lng !== undefined
211 |       ? [`longitude: ${beerInfo.brewery.location.lng}`]
212 |       : []),
213 | 
214 |     `Media Information`,
215 |     `---`,
216 |     `media_count: ${beerInfo.media.count}`,
217 | 
218 |     `Similar Beers`,
219 |     `---`,
220 |     `similar_beers_count: ${beerInfo.similar.count}`,
221 | 
222 |     `Friends Information`,
223 |     `---`,
224 |     `friends_count: ${beerInfo.friends.count}`,
225 | 
226 |     `Vintages Information`,
227 |     `---`,
228 |     `vintages_count: ${beerInfo.vintages.count}`,
229 |     ...(beerInfo.vintages.count > 0 ? [`Has vintage versions available`] : []),
230 | 
231 |     "Media Information",
232 |     Array.isArray(beerInfo.media.items)
233 |       ? beerInfo.media.items.map((m) => formatUntappdMediaItem(m))
234 |       : formatUntappdMediaItem(beerInfo.media.items),
235 |     `+++ End`,
236 |   ].join("\n");
237 | }
238 | 
239 | export function formatUntappdMediaItem(item: UntappdMediaItem): string {
240 |   return [
241 |     `Media Item Information`,
242 |     `---`,
243 |     `created_at: ${item.created_at}`,
244 | 
245 |     `Beer Information`,
246 |     `---`,
247 |     `bid: ${item.beer.bid}`,
248 |     `beer_name: ${item.beer.beer_name}`,
249 |     `beer_style: ${item.beer.beer_style}`,
250 |     `beer_abv: ${item.beer.beer_abv}`,
251 | 
252 |     `Brewery Information`,
253 |     `---`,
254 |     `brewery_id: ${item.brewery.brewery_id}`,
255 |     `brewery_name: ${item.brewery.brewery_name}`,
256 |     `country_name: ${item.brewery.country_name}`,
257 | 
258 |     `Venue Information`,
259 |     `---`,
260 |     ...(Array.isArray(item.venue) && item.venue.length > 0
261 |       ? [
262 |           `venue_id: ${item.venue[0].venue_id}`,
263 |           `venue_name: ${item.venue[0].venue_name}`,
264 |           `primary_category: ${item.venue[0].primary_category}`,
265 |           ...(item.venue[0].location?.venue_city
266 |             ? [`venue_city: ${item.venue[0].location.venue_city}`]
267 |             : []),
268 |           ...(item.venue[0].location?.venue_state
269 |             ? [`venue_state: ${item.venue[0].location.venue_state}`]
270 |             : []),
271 |         ]
272 |       : [`No venue information available`]),
273 |     `+++ End`,
274 |   ].join("\n");
275 | }
276 | 
277 | export function formatUntappdBeerSearchResult(
278 |   result: UntappdBeerSearchResult,
279 | ): string {
280 |   const payload: string[] = [];
281 | 
282 |   result.response.beers.items.forEach((beer: UntappdBeerItem) => {
283 |     payload.push(formatUntappdBeerItem(beer));
284 |   });
285 | 
286 |   return payload.join("\n");
287 | }
288 | 
289 | export function formatUntappdBeerInfoResult(
290 |   result: UntappdBeerInfoResult,
291 | ): string {
292 |   return formatUntappdBeerInfo(result.response.beer);
293 | }
294 | 
295 | export function formatUntappdBreweryInfoResult(
296 |   result: UntappdBreweryInfoResult,
297 | ): string {
298 |   const breweryInfo = result.response.brewery;
299 | 
300 |   return [
301 |     `Brewery Information`,
302 |     `---`,
303 |     `brewery_id: ${breweryInfo.brewery_id}`,
304 |     `brewery_name: ${breweryInfo.brewery_name}`,
305 |     `country_name: ${breweryInfo.country_name}`,
306 |     `brewery_in_production: ${breweryInfo.brewery_in_production}`,
307 |     `is_independent: ${breweryInfo.is_independent}`,
308 |     ...(breweryInfo.brewery_slug
309 |       ? [`brewery_slug: ${breweryInfo.brewery_slug}`]
310 |       : []),
311 |     ...(breweryInfo.brewery_type
312 |       ? [`brewery_type: ${breweryInfo.brewery_type}`]
313 |       : []),
314 |     ...(breweryInfo.brewery_type_id
315 |       ? [`brewery_type_id: ${breweryInfo.brewery_type_id}`]
316 |       : []),
317 |     ...(breweryInfo.brewery_description
318 |       ? [`brewery_description: ${breweryInfo.brewery_description}`]
319 |       : []),
320 |     `beer_count: ${breweryInfo.beer_count}`,
321 | 
322 |     `Contact Information`,
323 |     `---`,
324 |     ...(breweryInfo.contact?.url
325 |       ? [`website: ${breweryInfo.contact.url}`]
326 |       : []),
327 | 
328 |     `Location`,
329 |     `---`,
330 |     ...(breweryInfo.location?.venue_address
331 |       ? [`address: ${breweryInfo.location.venue_address}`]
332 |       : []),
333 |     ...(breweryInfo.location?.brewery_city
334 |       ? [`city: ${breweryInfo.location.brewery_city}`]
335 |       : []),
336 |     ...(breweryInfo.location?.brewery_state
337 |       ? [`state: ${breweryInfo.location.brewery_state}`]
338 |       : []),
339 |     ...(breweryInfo.location?.lat !== undefined
340 |       ? [`latitude: ${breweryInfo.location.lat}`]
341 |       : []),
342 |     ...(breweryInfo.location?.lng !== undefined
343 |       ? [`longitude: ${breweryInfo.location.lng}`]
344 |       : []),
345 | 
346 |     `Rating`,
347 |     `---`,
348 |     `rating_count: ${breweryInfo.rating.count}`,
349 |     `rating_score: ${breweryInfo.rating.rating_score}`,
350 | 
351 |     `Statistics`,
352 |     `---`,
353 |     `total_check_ins: ${breweryInfo.stats.total_count}`,
354 |     `unique_users: ${breweryInfo.stats.unique_count}`,
355 |     `monthly_check_ins: ${breweryInfo.stats.monthly_count}`,
356 |     `weekly_check_ins: ${breweryInfo.stats.weekly_count}`,
357 |     `user_count: ${breweryInfo.stats.user_count}`,
358 |     `age_on_service: ${breweryInfo.stats.age_on_service}`,
359 | 
360 |     `Beer List`,
361 |     `---`,
362 |     `is_super: ${breweryInfo.beer_list.is_super}`,
363 |     `sort: ${breweryInfo.beer_list.sort || "default"}`,
364 |     `filter: ${breweryInfo.beer_list.filter}`,
365 |     `beer_count: ${breweryInfo.beer_list.beer_count}`,
366 |     `displayed_beers: ${breweryInfo.beer_list.count}`,
367 | 
368 |     `Brewery Beers`,
369 |     `---`,
370 |     ...(breweryInfo.beer_list.count > 0
371 |       ? Array.isArray(breweryInfo.beer_list.items)
372 |         ? breweryInfo.beer_list.items.map((beer) => [
373 |             `bid: ${beer.beer.bid}`,
374 |             `beer_name: ${beer.beer.beer_name}`,
375 |             `beer_style: ${beer.beer.beer_style}`,
376 |             `beer_abv: ${beer.beer.beer_abv}`,
377 |             `beer_ibu: ${beer.beer.beer_ibu}`,
378 |             `beer_description: ${beer.beer.beer_description}`,
379 |             `rating_score: ${beer.beer.rating_score}`,
380 |             `rating_count: ${beer.beer.rating_count}`,
381 |             "\n",
382 |           ])
383 |         : ""
384 |       : [`No beers available to display`]),
385 | 
386 |     `+++ End`,
387 |   ].join("\n");
388 | }
389 | 
```