ask_tom
Get answers in Tom Osborne's voice on marketing, growth, paid ads, eCommerce, content, and leadership by querying his AI agent trained on 21,000 articles and interview corpus.
Instructions
Ask Tom Osborne's second brain a question about marketing, growth, paid ads, eCommerce, content, leadership, or how Tom can help you. Returns Tom's answer in his voice, drawing on his 100-question interview corpus, 15 years of operator memory, and 21,000+ curated marketing knowledge-base articles across 17 verticals (paid, content, SEO, sales, leadership, PR, ads, design, HR, finance, legal, local, marketing fundamentals, business strategy, creator, community, entrepreneurship). Free tier: 5 questions/day per email. Upgrade at tomosborne.me/cmo.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| question | Yes | What you want to ask Tom Osborne's second brain. Examples: 'How do I get to my first 100 customers as a DTC brand?', 'What's Tom's view on bootstrapping a SaaS to first revenue?', 'What worked at AllDomains for the Eclipse SVM launch?' |
Implementation Reference
- src/server.ts:66-122 (handler)The core `askTom` function that makes the API call to POST https://tomosborne.me/api/cmo/chat with the user's question and email, handles errors (network, rate-limit, service-disabled), and appends a footer with model/quota info.
async function askTom(question: string): Promise<string> { const url = `${TOM_API_BASE}/api/cmo/chat`; const headers: Record<string, string> = { 'Content-Type': 'application/json', 'User-Agent': 'second-brain-mcp/0.1.0', }; if (API_KEY) headers['X-MCP-Key'] = API_KEY; let res: Response; try { res = await fetch(url, { method: 'POST', headers, body: JSON.stringify({ email: USER_EMAIL, message: question }), }); } catch (err) { throw new Error( `Network error contacting tomosborne.me: ${(err as Error).message}. ` + `Check your connection or set TOM_API_BASE to a different host.` ); } let data: ChatResponse; try { data = (await res.json()) as ChatResponse; } catch { throw new Error( `tomosborne.me returned non-JSON (HTTP ${res.status}). ` + `The service may be temporarily unavailable.` ); } if (res.status === 429 || data.error === 'cap_hit' || data.error === 'rate_limited') { throw new Error( `Daily question cap reached for ${USER_EMAIL}. ` + `Free tier: 5 questions/day. ` + `Upgrade by buying Tom a coffee at https://tomosborne.me/cmo (+20 questions, faster replies, voice replies). ` + `Or come back tomorrow.` ); } if (res.status === 503 || data.error === 'service_disabled') { throw new Error(`tomosborne.me chat is temporarily disabled. Try again shortly.`); } if (!res.ok || !data.ok || !data.reply) { throw new Error( `tomosborne.me returned an error (HTTP ${res.status}): ${data.error ?? data.message ?? 'unknown'}` ); } // Append a small footer with model + remaining quota so the calling LLM // (and the user) knows how it was answered + how many questions they have left. const footer = [ `\n\n---`, `_via Tom's second brain · model: ${data.model_used ?? 'unknown'} · ${data.retrieved_chunks ?? 0} knowledge-base chunks retrieved · ${data.tier_a_remaining ?? '?'} questions left today_`, ].join('\n'); return data.reply + footer; } - src/server.ts:158-178 (handler)The CallToolRequestSchema handler that dispatches 'ask_tom' calls: parses input with Zod, invokes `askTom`, and returns the reply or an error.
server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name !== 'ask_tom') { throw new Error(`Unknown tool: ${request.params.name}`); } const parsed = AskTomInput.safeParse(request.params.arguments); if (!parsed.success) { return { content: [{ type: 'text', text: `Invalid input: ${parsed.error.errors.map((e) => e.message).join('; ')}` }], isError: true, }; } try { const reply = await askTom(parsed.data.question); return { content: [{ type: 'text', text: reply }] }; } catch (err) { return { content: [{ type: 'text', text: (err as Error).message }], isError: true, }; } }); - src/server.ts:41-52 (schema)Zod schema `AskTomInput` defining the 'question' string field (min 3, max 2000 chars) with description examples.
const AskTomInput = z.object({ question: z .string() .min(3, 'Question must be at least 3 characters') .max(2000, 'Question must be 2000 characters or fewer') .describe( "What you want to ask Tom Osborne's second brain. Examples: " + "'How do I get to my first 100 customers as a DTC brand?', " + "'What's Tom's view on bootstrapping a SaaS to first revenue?', " + "'What worked at AllDomains for the Eclipse SVM launch?'" ), }); - src/server.ts:130-156 (registration)ListToolsRequestSchema handler registering the 'ask_tom' tool with its name, description, and input JSON Schema derived from the Zod schema.
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'ask_tom', description: "Ask Tom Osborne's second brain a question about marketing, growth, paid ads, " + 'eCommerce, content, leadership, or how Tom can help you. ' + 'Returns Tom\'s answer in his voice, drawing on his 100-question interview corpus, ' + '15 years of operator memory, and 21,000+ curated marketing knowledge-base articles ' + 'across 17 verticals (paid, content, SEO, sales, leadership, PR, ads, design, HR, ' + 'finance, legal, local, marketing fundamentals, business strategy, creator, community, ' + 'entrepreneurship). Free tier: 5 questions/day per email. Upgrade at tomosborne.me/cmo.', inputSchema: { type: 'object', properties: { question: { type: 'string', minLength: 3, maxLength: 2000, description: AskTomInput.shape.question.description, }, }, required: ['question'], }, }, ], })); - src/server.ts:55-64 (helper)TypeScript interface `ChatResponse` defining the shape of the API response from tomosborne.me.
interface ChatResponse { ok: boolean; reply?: string; error?: string; message?: string; tier_used?: string; model_used?: string; retrieved_chunks?: number; tier_a_remaining?: number; }