# 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 |
```