@buygit/mcp-server 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +7 -7
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{StdioServerTransport as
|
|
2
|
+
import{StdioServerTransport as ie}from"@modelcontextprotocol/sdk/server/stdio.js";import{Server as K}from"@modelcontextprotocol/sdk/server/index.js";import{CallToolRequestSchema as Q,ListToolsRequestSchema as Z,ListResourceTemplatesRequestSchema as ee,ListResourcesRequestSchema as te,ReadResourceRequestSchema as re,ListPromptsRequestSchema as ne,GetPromptRequestSchema as se}from"@modelcontextprotocol/sdk/types.js";import{z as a}from"zod";import{Pool as E}from"undici";var d="0.1.1",f="@buygit/mcp-server";var I=process.env.BUYGIT_API_BASE||"https://buygit.com",A=`@buygit/mcp-server/${d} (+https://buygit.com/mcp)`,$=null;function j(e){return $===null&&($=new E(e,{connections:4,keepAliveTimeout:6e4,keepAliveMaxTimeout:6e5})),$}var b=class extends Error{constructor(r,n,s){super(r);this.status=n;this.code=s;this.name="BuygitApiError"}status;code};async function u(e,t={},r={}){let s=j(I),i=Object.entries(t).filter(([,h])=>h!=null&&h!=="").map(([h,B])=>`${encodeURIComponent(h)}=${encodeURIComponent(String(B))}`).join("&"),o=i?`${e}?${i}`:e,g=await s.request({method:"GET",path:o,headers:{"user-agent":A,accept:"application/json"},headersTimeout:r.timeoutMs??12e3,bodyTimeout:r.timeoutMs??12e3}),c=g.statusCode,p=await g.body.text(),m=null;try{m=JSON.parse(p)}catch{throw new b(`upstream returned non-JSON (status ${c})`,c)}if(c>=400||m.ok===!1)throw new b(m.error??`upstream returned HTTP ${c}`,c,m.code);return m.data!==void 0?m.data:m}function S(e){return e>=1e3?`${(e/1e3).toFixed(1)}k`:String(e)}function T(e){return e.filter(t=>!!(t&&t.length>0)).join(" \xB7 ")}function G(e,t){let r=t!==void 0?`${t}. **${e.title}**`:`**${e.title}**`,n=T([e.stars>0?`\u2605\xA0${S(e.stars)}`:null,e.license||null,e.language||null,e.category?e.category.name:null]);return[r,n?` ${n}`:null,` ${e.short_description}`,` ${e.url}`].filter(s=>s!==null).join(`
|
|
3
3
|
`)}function _(e,t){let r=t?.header?`${t.header}
|
|
4
4
|
|
|
5
5
|
`:"",n=t?.footer?`
|
|
6
6
|
|
|
7
|
-
${t.footer}`:"";return e.length===0?`${r}_No results._${n}`:r+e.map((s,i)=>
|
|
7
|
+
${t.footer}`:"";return e.length===0?`${r}_No results._${n}`:r+e.map((s,i)=>G(s,i+1)).join(`
|
|
8
8
|
|
|
9
9
|
`)+n}function w(e){let t=[];t.push(`# ${e.title}`),t.push(""),t.push(T([e.repo_signals.stars>0?`\u2605 ${S(e.repo_signals.stars)}`:null,e.license,e.repo_signals.language,e.category?.name??null,e.source])),t.push(""),t.push(e.short_description),t.push(""),e.tags&&e.tags.length>0&&(t.push(`**Tags:** ${e.tags.slice(0,12).map(s=>`\`${s}\``).join(" ")}`),t.push(""));let r=[];e.safety_signals.secret_scan==="clean"?r.push("secret-scan: clean \u2713"):e.safety_signals.secret_scan==="flagged"?r.push("secret-scan: **flagged** \u26A0"):r.push("secret-scan: not run"),e.safety_signals.malware_flag&&r.push("**malware flag set** \u26A0"),e.repo_signals.upstream_status==="removed"&&r.push("**upstream removed** \u26A0"),t.push(`**Safety:** ${r.join(" \xB7 ")}`),t.push("");let n=[];if(e.repo_signals.repo_url&&n.push(`repo: ${e.repo_signals.repo_url}`),e.repo_signals.default_branch&&n.push(`branch: \`${e.repo_signals.default_branch}\``),e.repo_signals.last_commit_at&&n.push(`last commit: ${e.repo_signals.last_commit_at.slice(0,10)}`),e.repo_signals.good_first_issues>0&&n.push(`good-first-issues: ${e.repo_signals.good_first_issues}`),n.length>0&&(t.push(`**Repo:** ${n.join(" \xB7 ")}`),t.push("")),e.full_description_md&&e.full_description_md.trim().length>0){let s=e.full_description_md.slice(0,1200);t.push("## Description"),t.push(s+(e.full_description_md.length>1200?`
|
|
10
10
|
|
|
11
11
|
_(truncated \u2014 see canonical page for full text)_`:"")),t.push("")}if(t.push(`**BuyGit listing:** ${e.url}`),e.attribution&&(t.push(""),t.push(`_Attribution:_ ${e.attribution}`)),e.similar&&e.similar.length>0){t.push(""),t.push("## Similar listings");for(let s of e.similar.slice(0,5))t.push(`- **${s.title}** \xB7 ${s.url}`)}return t.join(`
|
|
12
12
|
`)}function R(e){let t=["# Side-by-side comparison",""];for(let r of e){if("error"in r){t.push(`## \`${r.slug}\``),t.push("_not found in the crawler index_"),t.push("");continue}t.push(`## ${r.title}`),t.push(T([r.repo_signals.stars>0?`\u2605 ${S(r.repo_signals.stars)}`:null,r.license,r.repo_signals.language,r.category?.name??null])),t.push(""),t.push(r.short_description),t.push(""),t.push(`\u2192 ${r.url}`),t.push("")}return t.join(`
|
|
13
|
-
`)}function y(e){return{content:[{type:"text",text:e}]}}function l(e){return{content:[{type:"text",text:e instanceof b?`BuyGit API error (${e.status}${e.code?` / ${e.code}`:""}): ${e.message}`:e instanceof Error?`Tool error: ${e.message}`:`Tool error: ${String(e)}`}],isError:!0}}var
|
|
14
|
-
`))}catch(t){return l(t)}}},
|
|
15
|
-
`))}catch(e){return l(e)}}},
|
|
13
|
+
`)}function y(e){return{content:[{type:"text",text:e}]}}function l(e){return{content:[{type:"text",text:e instanceof b?`BuyGit API error (${e.status}${e.code?` / ${e.code}`:""}): ${e.message}`:e instanceof Error?`Tool error: ${e.message}`:`Tool error: ${String(e)}`}],isError:!0}}var O=a.object({query:a.string().max(200).optional(),category:a.string().max(80).optional(),language:a.string().max(40).optional(),license:a.string().max(40).optional(),min_stars:a.number().int().min(0).optional(),limit:a.number().int().min(1).max(50).default(10),sort:a.enum(["relevance","newest","stars","health"]).default("relevance")}),D={name:"buygit_search",description:'Search the BuyGit Open Index (76k+ crawler-imported Git projects). Supports free-text query plus filters (category slug, language, SPDX license, min stars) and four sort modes (relevance / newest / stars / health). Returns up to 50 compact Markdown summaries, each with a canonical BuyGit URL the user can click. Use this first when the user asks "find me a repo for ...".',inputSchema:{type:"object",properties:{query:{type:"string",maxLength:200,description:"Free text query."},category:{type:"string",maxLength:80,description:"Category slug."},language:{type:"string",maxLength:40,description:'Primary language e.g. "TypeScript".'},license:{type:"string",maxLength:40,description:'SPDX id e.g. "MIT".'},min_stars:{type:"integer",minimum:0},limit:{type:"integer",minimum:1,maximum:50,default:10},sort:{type:"string",enum:["relevance","newest","stars","health"],default:"relevance"}}},handler:async e=>{let t=O.safeParse(e??{});if(!t.success)return l(`invalid arguments: ${t.error.message}`);let{query:r,category:n,language:s,license:i,min_stars:o,limit:g,sort:c}=t.data;try{let p=await u("/api/v1/crawler/search",{q:r,category:n,language:s,license:i,min_stars:o,limit:g,sort:c}),m=[`**${p.results.length} result(s)**`,r?`for "${r}"`:null,n?`\xB7 category: ${n}`:null,s?`\xB7 lang: ${s}`:null,i?`\xB7 license: ${i}`:null,o?`\xB7 \u2265${o}\u2605`:null,`\xB7 sort: ${c}`].filter(Boolean).join(" "),h=p.has_more?"_More results available. Call again with a narrower filter, or use buygit_get_listing(slug) on any of these for full detail._":"_Call buygit_get_listing(slug) on any result for full detail._";return y(_(p.results,{header:m,footer:h}))}catch(p){return l(p)}}},C=a.object({slug:a.string().regex(/^[a-z0-9\-]+$/i).max(200)}),U={name:"buygit_get_listing",description:"Retrieve the full BuyGit Open Index detail for one listing by slug. Includes safety signals (secret scan status, malware flag, upstream health), repo signals (stars, forks, language, last commit), license + attribution, and up to 5 similar listings. Slug must come from a previous buygit_search / trending / random / compare result.",inputSchema:{type:"object",required:["slug"],properties:{slug:{type:"string",pattern:"^[a-z0-9\\-]+$",maxLength:200,description:"Listing slug from a previous tool call."}}},handler:async e=>{let t=C.safeParse(e??{});if(!t.success)return l(`invalid slug: ${t.error.message}`);try{let r=await u(`/api/v1/crawler/listings/${encodeURIComponent(t.data.slug)}`),n={...r,rating_block:r.rating??{avg:null,count:0}};return y(w(n))}catch(r){return l(r)}}},M={name:"buygit_list_categories",description:"List the BuyGit Open Index category taxonomy. Each node carries the count of crawler listings in that branch \u2014 useful for narrowing a search or picking a category slug for `buygit_search`.",inputSchema:{type:"object",properties:{}},handler:async()=>{try{let n=function(s,i){let o=" ".repeat(i);r.push(`${o}- **${s.name}** \`${s.slug}\` \xB7 ${s.crawler_listing_count}`);for(let g of s.children)n(g,i+1)};var e=n;let t=await u("/api/v1/crawler/categories"),r=["**BuyGit category tree** (crawler listings only)",""];for(let s of t.categories)n(s,0);return r.push(""),r.push("_Pick a slug and pass it as `category` to `buygit_search` to filter._"),y(r.join(`
|
|
14
|
+
`))}catch(t){return l(t)}}},N=a.object({period:a.enum(["day","week","month"]).default("week"),category:a.string().max(80).optional(),limit:a.number().int().min(1).max(50).default(10)}),F={name:"buygit_trending",description:"Top crawler listings by recent repository activity within a period (day / week / month). Optionally narrow to a single category. Ranked primarily by current stars among recently-committed repos.",inputSchema:{type:"object",properties:{period:{type:"string",enum:["day","week","month"],default:"week"},category:{type:"string",maxLength:80},limit:{type:"integer",minimum:1,maximum:50,default:10}}},handler:async e=>{let t=N.safeParse(e??{});if(!t.success)return l(t.error.message);let{period:r,category:n,limit:s}=t.data;try{let i=await u("/api/v1/crawler/trending",{period:r,category:n,limit:s}),o=`**Trending (${r}${n?` \xB7 ${n}`:""})** \u2014 ${i.count} listing(s)`;return y(_(i.results,{header:o,footer:"_Call buygit_get_listing(slug) for full detail._"}))}catch(i){return l(i)}}},H=a.object({slugs:a.array(a.string().regex(/^[a-z0-9\-]+$/i)).min(2).max(5)}),z={name:"buygit_compare",description:"Side-by-side comparison of 2-5 crawler listings. Pass slugs you got from a previous tool call. Slugs that don't exist appear as `not found` entries.",inputSchema:{type:"object",required:["slugs"],properties:{slugs:{type:"array",items:{type:"string",pattern:"^[a-z0-9\\-]+$"},minItems:2,maxItems:5}}},handler:async e=>{let t=H.safeParse(e??{});if(!t.success)return l(t.error.message);try{let n=(await u("/api/v1/crawler/compare",{slugs:t.data.slugs.join(",")})).items.map(s=>{if("error"in s)return s;let i=s;return{...i,rating_block:i.rating??{avg:null,count:0}}});return y(R(n))}catch(r){return l(r)}}},Y={name:"buygit_stats",description:'BuyGit Open Index meta \u2014 total listings, breakdown by license / category / source, last_indexed_at. Useful for "how big is the catalog?" questions.',inputSchema:{type:"object",properties:{}},handler:async()=>{try{let e=await u("/api/v1/crawler/stats"),t=[];t.push(`**BuyGit Open Index \u2014 ${e.total_listings.toLocaleString()} crawler listings**`),t.push(""),e.last_indexed_at&&t.push(`Last indexed: ${e.last_indexed_at}`),t.push(""),t.push("**Top licenses**");for(let r of e.by_license.slice(0,8))t.push(`- ${r.license}: ${r.count.toLocaleString()}`);t.push(""),t.push("**Top categories**");for(let r of e.by_category.slice(0,10))t.push(`- ${r.name} \`${r.slug}\`: ${r.count.toLocaleString()}`);t.push(""),t.push("**Sources**");for(let r of e.by_source.slice(0,10))t.push(`- ${r.source}: ${r.count.toLocaleString()}`);return y(t.join(`
|
|
15
|
+
`))}catch(e){return l(e)}}},V=a.object({count:a.number().int().min(1).max(10).default(1),category:a.string().max(80).optional()}),J={name:"buygit_random",description:'Surface 1-10 random crawler listings \u2014 useful for "surprise me" prompts or browsing a category casually. Optional `category` slug narrows the pool.',inputSchema:{type:"object",properties:{count:{type:"integer",minimum:1,maximum:10,default:1},category:{type:"string",maxLength:80}}},handler:async e=>{let t=V.safeParse(e??{});if(!t.success)return l(t.error.message);try{let r=await u("/api/v1/crawler/random",{count:t.data.count,category:t.data.category});return y(_(r.results,{header:`**Random pick (${r.count})**`}))}catch(r){return l(r)}}},X=a.object({query:a.string().min(1).max(200).describe("A library / repo name or short description of what you want an alternative to."),language:a.string().max(40).optional(),license:a.string().max(40).optional(),limit:a.number().int().min(1).max(20).default(8)}),W={name:"buygit_find_alternative",description:'Find BuyGit Open Index listings that are conceptual alternatives to a library, repo, or short description. Wraps `buygit_search` with sensible defaults (sort by stars, exclude the input name from matches if obvious). Use when the user says "what can replace X?" or "alternatives to Y?".',inputSchema:{type:"object",required:["query"],properties:{query:{type:"string",minLength:1,maxLength:200,description:"Library or repo to find alternatives for."},language:{type:"string",maxLength:40},license:{type:"string",maxLength:40},limit:{type:"integer",minimum:1,maximum:20,default:8}}},handler:async e=>{let t=X.safeParse(e??{});if(!t.success)return l(t.error.message);let{query:r,language:n,license:s,limit:i}=t.data;try{let o=await u("/api/v1/crawler/search",{q:r,language:n,license:s,limit:i,sort:"stars"}),g=o.results.filter(p=>!p.title.toLowerCase().includes(r.toLowerCase().split(/[\s/]/)[0]??"")),c=g.length>=3?g:o.results;return y(_(c,{header:`**Alternatives to "${r}"** ${n?`(${n})`:""}`.trim(),footer:"_If none of these fit, try `buygit_search` with a more specific query._"}))}catch(o){return l(o)}}},L=[D,U,M,F,z,Y,J,W];var k=[{uriTemplate:"buygit://listing/{slug}",name:"BuyGit listing",description:"Full Markdown detail for a single crawler-imported listing. Replace {slug} with the slug from a previous search/trending result.",mimeType:"text/markdown"},{uriTemplate:"buygit://category/{slug}",name:"BuyGit category top listings",description:"Markdown summary of a category \u2014 the top 20 crawler listings by stars within it. Replace {slug} with a category slug from buygit_list_categories.",mimeType:"text/markdown"}];async function P(e){let t=/^buygit:\/\/(listing|category)\/([a-z0-9\-]+)$/i.exec(e);if(!t)throw new Error(`unsupported resource uri: ${e}`);let[,r,n]=t;if(r==="listing"){let i=await u(`/api/v1/crawler/listings/${encodeURIComponent(n)}`),o={...i,rating_block:i.rating??{avg:null,count:0}};return{uri:e,mimeType:"text/markdown",text:w(o)}}let s=await u("/api/v1/crawler/search",{category:n,sort:"stars",limit:20});return{uri:e,mimeType:"text/markdown",text:_(s.results,{header:`**Category: \`${n}\` \u2014 top 20 by stars**`,footer:s.has_more?"_More listings exist \u2014 use buygit_search with this category to paginate._":void 0})}}var v=[{name:"starter_for_stack",description:"Recommend BuyGit Open Index starter kits / templates for a given stack.",arguments:[{name:"stack",description:'Tech stack the user is building on (e.g. "Next.js + Supabase + Stripe").',required:!0},{name:"budget",description:"Free / under_50 / under_200 / any (default any).",required:!1}],render:e=>({description:`Find starter kits for: ${e.stack}`,messages:[{role:"user",content:{type:"text",text:`I'm building a project on **${e.stack}**${e.budget?` (budget: ${e.budget})`:""}.
|
|
16
16
|
|
|
17
17
|
Use \`buygit_search\` against the BuyGit Open Index \u2014 start with the stack name as the query, then refine with \`category\` or \`language\` if the first page is noisy. Aim for repos that:
|
|
18
18
|
|
|
@@ -33,5 +33,5 @@ For each suggestion, briefly explain how it compares (smaller / larger / newer /
|
|
|
33
33
|
|
|
34
34
|
1. If the input isn't a clean slug, call \`buygit_list_categories\` and pick the closest match.
|
|
35
35
|
2. Then call \`buygit_trending\` with the resolved slug and period=week to surface what's hot.
|
|
36
|
-
3. Summarise the top 5 in one sentence each. Include BuyGit URLs.`}}]})}];function q(){let e=new
|
|
37
|
-
`)}async function
|
|
36
|
+
3. Summarise the top 5 in one sentence each. Include BuyGit URLs.`}}]})}];function q(){let e=new K({name:f,version:d},{capabilities:{tools:{},resources:{},prompts:{}}});return e.setRequestHandler(Z,async()=>({tools:L.map(t=>({name:t.name,description:t.description,inputSchema:t.inputSchema}))})),e.setRequestHandler(Q,async t=>{let{name:r,arguments:n}=t.params,s=L.find(i=>i.name===r);return s?s.handler(n??{}):{content:[{type:"text",text:`Unknown tool: ${r}`}],isError:!0}}),e.setRequestHandler(ee,async()=>({resourceTemplates:k})),e.setRequestHandler(te,async()=>({resources:[]})),e.setRequestHandler(re,async t=>{let{uri:r}=t.params;try{return{contents:[await P(r)]}}catch(n){throw new Error(`failed to read resource ${r}: ${n.message}`)}}),e.setRequestHandler(ne,async()=>({prompts:v.map(t=>({name:t.name,description:t.description,arguments:t.arguments}))})),e.setRequestHandler(se,async t=>{let{name:r,arguments:n}=t.params,s=v.find(i=>i.name===r);if(!s)throw new Error(`unknown prompt: ${r}`);return s.render(n??{})}),e}function x(...e){process.stderr.write(e.map(t=>typeof t=="string"?t:JSON.stringify(t)).join(" ")+`
|
|
37
|
+
`)}async function ae(){let e=(process.env.BUYGIT_MCP_TRANSPORT??"stdio").toLowerCase(),t=q();if(e==="stdio"){let r=new ie;await t.connect(r),x(`[${f}@${d}] connected via stdio`);return}e==="http"&&(x(`[${f}@${d}] HTTP transport not yet implemented in v0.1; stdio is the supported mode. Set BUYGIT_MCP_TRANSPORT=stdio.`),process.exit(2)),x(`[${f}@${d}] unknown transport "${e}". Set BUYGIT_MCP_TRANSPORT to "stdio" (default) or "http".`),process.exit(2)}ae().catch(e=>{x(`[${f}@${d}] fatal: ${e.message}`),process.exit(1)});
|
package/package.json
CHANGED