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

```
├── .dockerignore
├── .editorconfig
├── .github
│   ├── CODE_OF_CONDUCT.md
│   ├── CODEOWNERS
│   ├── CONTRIBUTING.md
│   ├── Dockerfile
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.yml
│   │   ├── config.yml
│   │   └── feature_request.yml
│   ├── PULL_REQUEST_TEMPLATE.md
│   ├── SECURITY.md
│   ├── SUPPORT.md
│   └── workflows
│       ├── docker-prune.yml
│       ├── generate-readme.yml
│       ├── release.yml
│       ├── renovate.yml
│       ├── tag-semver.yml
│       ├── test.yaml
│       └── triage.yml
├── .gitignore
├── .golangci.yaml
├── go.mod
├── go.sum
├── http.go
├── internal
│   ├── api
│   │   ├── client.go
│   │   ├── list_libraries.go
│   │   ├── request.go
│   │   └── search_libraries.go
│   └── mcpserver
│       ├── logging.go
│       ├── resource_libraries.go
│       ├── server.go
│       ├── templates
│       │   ├── resolve_library_id_desc.gotmpl
│       │   ├── resolve_library_id_resp.gotmpl
│       │   └── search_library_docs_desc.gotmpl
│       ├── templates.go
│       ├── tool_resolve_library_id.go
│       ├── tool_search_library_docs.go
│       └── utils.go
├── LICENSE
├── main.go
├── Makefile
└── README.md
```

# Files

--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------

```
 1 | *.log
 2 | ~*
 3 | *.tmp
 4 | *.txt
 5 | dist
 6 | *.test
 7 | *.prof
 8 | *.conf
 9 | *.toml
10 | .git
11 | bin
12 | **/.env
13 | context7-http
14 | 
```

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

```
 1 | *.log
 2 | ~*
 3 | *.tmp
 4 | tmp
 5 | *.txt
 6 | dist
 7 | *.test
 8 | *.prof
 9 | *.conf
10 | **/.env
11 | **/bin/**
12 | *.out
13 | context7-http
14 | 
```

--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------

```
 1 | # THIS FILE IS GENERATED! DO NOT EDIT! Maintained by Terraform.
 2 | #
 3 | # editorconfig:  https://editorconfig.org/
 4 | # actual source: https://github.com/lrstanley/.github/blob/master/terraform/github-common-files/templates/.editorconfig
 5 | #
 6 | 
 7 | root = true
 8 | 
 9 | [*]
10 | charset = utf-8
11 | end_of_line = lf
12 | indent_size = 4
13 | indent_style = space
14 | insert_final_newline = true
15 | trim_trailing_whitespace = true
16 | max_line_length = 100
17 | 
18 | [*.tf]
19 | indent_size = 2
20 | 
21 | [*.go]
22 | indent_style = tab
23 | indent_size = 4
24 | 
25 | [*.md]
26 | trim_trailing_whitespace = false
27 | 
28 | [*.{md,py,sh,yml,yaml,cjs,js,ts,vue,css}]
29 | max_line_length = 105
30 | 
31 | [*.{yml,yaml,toml}]
32 | indent_size = 2
33 | 
34 | [*.json]
35 | indent_size = 2
36 | 
37 | [*.html]
38 | max_line_length = 140
39 | indent_size = 2
40 | 
41 | [*.{cjs,js,ts,vue,css}]
42 | indent_size = 2
43 | 
44 | [Makefile]
45 | indent_style = tab
46 | 
47 | [**.min.js]
48 | indent_style = ignore
49 | 
50 | [*.bat]
51 | indent_style = tab
52 | 
```

--------------------------------------------------------------------------------
/.golangci.yaml:
--------------------------------------------------------------------------------

```yaml
  1 | # THIS FILE IS GENERATED! DO NOT EDIT! Maintained by Terraform.
  2 | #
  3 | # golangci-lint:       https://golangci-lint.run/
  4 | # false-positives:     https://golangci-lint.run/usage/false-positives/
  5 | # actual source:       https://github.com/lrstanley/.github/blob/master/terraform/github-common-files/templates/.golangci.yml
  6 | # modified variant of: https://gist.github.com/maratori/47a4d00457a92aa426dbd48a18776322
  7 | 
  8 | version: "2"
  9 | 
 10 | formatters:
 11 |   enable: [gofumpt]
 12 | 
 13 | issues:
 14 |   max-issues-per-linter: 0
 15 |   max-same-issues: 50
 16 | 
 17 | severity:
 18 |   default: error
 19 |   rules:
 20 |     - linters:
 21 |         - errcheck
 22 |         - gocritic
 23 |       severity: warning
 24 | 
 25 | linters:
 26 |   default: none
 27 |   enable:
 28 |     - asasalint # checks for pass []any as any in variadic func(...any)
 29 |     - asciicheck # checks that your code does not contain non-ASCII identifiers
 30 |     - bidichk # checks for dangerous unicode character sequences
 31 |     - bodyclose # checks whether HTTP response body is closed successfully
 32 |     - canonicalheader # checks whether net/http.Header uses canonical header
 33 |     - copyloopvar # detects places where loop variables are copied (Go 1.22+)
 34 |     - depguard # checks if package imports are in a list of acceptable packages
 35 |     - dupl # tool for code clone detection
 36 |     - durationcheck # checks for two durations multiplied together
 37 |     - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases
 38 |     - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error
 39 |     - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13
 40 |     - exhaustive # checks exhaustiveness of enum switch statements
 41 |     - exptostd # detects functions from golang.org/x/exp/ that can be replaced by std functions
 42 |     - fatcontext # detects nested contexts in loops
 43 |     - forbidigo # forbids identifiers
 44 |     - funlen # tool for detection of long functions
 45 |     - gocheckcompilerdirectives # validates go compiler directive comments (//go:)
 46 |     - gochecknoinits # checks that no init functions are present in Go code
 47 |     - gochecksumtype # checks exhaustiveness on Go "sum types"
 48 |     - gocognit # computes and checks the cognitive complexity of functions
 49 |     - goconst # finds repeated strings that could be replaced by a constant
 50 |     - gocritic # provides diagnostics that check for bugs, performance and style issues
 51 |     - godot # checks if comments end in a period
 52 |     - gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod
 53 |     - goprintffuncname # checks that printf-like functions are named with f at the end
 54 |     - gosec # inspects source code for security problems
 55 |     - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string
 56 |     - iface # checks the incorrect use of interfaces, helping developers avoid interface pollution
 57 |     - ineffassign # detects when assignments to existing variables are not used
 58 |     - intrange # finds places where for loops could make use of an integer range
 59 |     - loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap)
 60 |     - makezero # finds slice declarations with non-zero initial length
 61 |     - mirror # reports wrong mirror patterns of bytes/strings usage
 62 |     - misspell # [useless] finds commonly misspelled English words in comments
 63 |     - musttag # enforces field tags in (un)marshaled structs
 64 |     - nakedret # finds naked returns in functions greater than a specified function length
 65 |     - nestif # reports deeply nested if statements
 66 |     - nilerr # finds the code that returns nil even if it checks that the error is not nil
 67 |     - nilnesserr # reports that it checks for err != nil, but it returns a different nil value error (powered by nilness and nilerr)
 68 |     - nilnil # checks that there is no simultaneous return of nil error and an invalid value
 69 |     - noctx # finds sending http request without context.Context
 70 |     - nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL
 71 |     - perfsprint # checks that fmt.Sprintf can be replaced with a faster alternative
 72 |     - predeclared # finds code that shadows one of Go's predeclared identifiers
 73 |     - promlinter # checks Prometheus metrics naming via promlint
 74 |     - reassign # checks that package variables are not reassigned
 75 |     - recvcheck # checks for receiver type consistency
 76 |     - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint
 77 |     - rowserrcheck # checks whether Err of rows is checked successfully
 78 |     - sloglint # ensure consistent code style when using log/slog
 79 |     - sqlclosecheck # checks that sql.Rows and sql.Stmt are closed
 80 |     - staticcheck # is a go vet on steroids, applying a ton of static analysis checks
 81 |     - testableexamples # checks if examples are testable (have an expected output)
 82 |     - testifylint # checks usage of github.com/stretchr/testify
 83 |     - tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes
 84 |     - unconvert # removes unnecessary type conversions
 85 |     - unparam # reports unused function parameters
 86 |     - unused # checks for unused constants, variables, functions and types
 87 |     - usestdlibvars # detects the possibility to use variables/constants from the Go standard library
 88 |     - usetesting # reports uses of functions with replacement inside the testing package
 89 |     - wastedassign # finds wasted assignment statements
 90 |     - whitespace # detects leading and trailing whitespace
 91 | 
 92 |   settings:
 93 |     gocognit:
 94 |       min-complexity: 40
 95 |     errcheck:
 96 |       check-type-assertions: true
 97 |     funlen:
 98 |       lines: 150
 99 |       statements: 75
100 |       ignore-comments: true
101 |     gocritic:
102 |       disabled-checks:
103 |         - whyNoLint
104 |         - hugeParam
105 |         - ifElseChain
106 |         - singleCaseSwitch
107 |       enabled-tags:
108 |         - diagnostic
109 |         - opinionated
110 |         - performance
111 |         - style
112 |       settings:
113 |         captLocal:
114 |           paramsOnly: false
115 |         underef:
116 |           skipRecvDeref: false
117 |         rangeValCopy:
118 |           sizeThreshold: 512
119 |     depguard:
120 |       rules:
121 |         "deprecated":
122 |           files: ["$all"]
123 |           deny:
124 |             - pkg: github.com/golang/protobuf
125 |               desc: Use google.golang.org/protobuf instead, see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules
126 |             - pkg: github.com/satori/go.uuid
127 |               desc: Use github.com/google/uuid instead, satori's package is not maintained
128 |             - pkg: github.com/gofrs/uuid$
129 |               desc: Use github.com/gofrs/uuid/v5 or later, it was not a go module before v5
130 |             - pkg: github.com/lrstanley/clix$
131 |               desc: Use github.com/lrstanley/clix/v2 instead
132 |             - pkg: github.com/lrstanley/chix$
133 |               desc: Use github.com/lrstanley/chix/v2 instead
134 |             - pkg: log$
135 |               desc: Use log/slog instead, see https://go.dev/blog/slog
136 |         "non-test files":
137 |           files: ["!$test"]
138 |           deny:
139 |             - pkg: math/rand$
140 |               desc: Use math/rand/v2 instead, see https://go.dev/blog/randv2
141 |         "incorrect import":
142 |           files: ["$test"]
143 |           deny:
144 |             - pkg: github.com/tj/assert$
145 |               desc: Use github.com/stretchr/testify/assert instead, see
146 |     gochecksumtype:
147 |       default-signifies-exhaustive: false
148 |     exhaustive:
149 |       check:
150 |         - switch
151 |         - map
152 |     govet:
153 |       disable:
154 |         - fieldalignment
155 |       enable-all: true
156 |       settings:
157 |         shadow:
158 |           strict: true
159 |     perfsprint:
160 |       strconcat: false
161 |     nakedret:
162 |       max-func-lines: 0
163 |     nestif:
164 |       min-complexity: 10
165 |     rowserrcheck:
166 |       packages:
167 |         - github.com/jmoiron/sqlx
168 |     sloglint:
169 |       no-global: default
170 |       context: scope
171 |       msg-style: lowercased
172 |       static-msg: true
173 |       forbidden-keys:
174 |         - time
175 |         - level
176 |         - source
177 |     staticcheck:
178 |       checks:
179 |         - all
180 |         # Incorrect or missing package comment: https://staticcheck.dev/docs/checks/#ST1000
181 |         - -ST1000
182 |         # Use consistent method receiver names: https://staticcheck.dev/docs/checks/#ST1016
183 |         - -ST1016
184 |         # Omit embedded fields from selector expression: https://staticcheck.dev/docs/checks/#QF1008
185 |         - -QF1008
186 |         # duplicate struct tags -- used commonly for things like go-flags.
187 |         - -SA5008
188 |     usetesting:
189 |       os-temp-dir: true
190 |   exclusions:
191 |     warn-unused: true
192 |     generated: lax
193 |     presets:
194 |       - common-false-positives
195 |       - std-error-handling
196 |     paths:
197 |       - ".*\\.gen\\.go$"
198 |       - ".*\\.gen_test\\.go$"
199 |     rules:
200 |       - source: "TODO"
201 |         linters: [godot]
202 |       - text: "should have a package comment"
203 |         linters: [revive]
204 |       - text: 'exported \S+ \S+ should have comment( \(or a comment on this block\))? or be unexported'
205 |         linters: [revive]
206 |       - text: 'package comment should be of the form ".+"'
207 |         source: "// ?(nolint|TODO)"
208 |         linters: [revive]
209 |       - text: 'comment on exported \S+ \S+ should be of the form ".+"'
210 |         source: "// ?(nolint|TODO)"
211 |         linters: [revive, staticcheck]
212 |       - text: 'unexported-return: exported func \S+ returns unexported type \S+ .*'
213 |         linters: [revive]
214 |       - text: "var-declaration: should drop .* from declaration of .*; it is the zero value"
215 |         linters: [revive]
216 |       - text: ".*use ALL_CAPS in Go names.*"
217 |         linters: [revive, staticcheck]
218 |       - text: '.* always receives \S+'
219 |         linters: [unparam]
220 |       - path: _test\.go
221 |         linters:
222 |           - bodyclose
223 |           - dupl
224 |           - funlen
225 |           - gocognit
226 |           - goconst
227 |           - gosec
228 |           - noctx
229 |           - wrapcheck
230 | 
```

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

```markdown
  1 | <!-- template:define:options
  2 | {
  3 |   "nodescription": true
  4 | }
  5 | -->
  6 | ![logo](https://liam.sh/-/gh/svg/lrstanley/context7-http?layout=left&icon=fluent-emoji-flat%3Amagic-wand&icon.width=60&bg=geometric)
  7 | 
  8 | <!-- template:begin:header -->
  9 | <!-- do not edit anything in this "template" block, its auto-generated -->
 10 | 
 11 | <p align="center">
 12 |   <a href="https://github.com/lrstanley/context7-http/releases">
 13 |     <img title="Release Downloads" src="https://img.shields.io/github/downloads/lrstanley/context7-http/total?style=flat-square">
 14 |   </a>
 15 |   <a href="https://github.com/lrstanley/context7-http/tags">
 16 |     <img title="Latest Semver Tag" src="https://img.shields.io/github/v/tag/lrstanley/context7-http?style=flat-square">
 17 |   </a>
 18 |   <a href="https://github.com/lrstanley/context7-http/commits/master">
 19 |     <img title="Last commit" src="https://img.shields.io/github/last-commit/lrstanley/context7-http?style=flat-square">
 20 |   </a>
 21 | 
 22 | 
 23 | 
 24 | 
 25 | 
 26 |   <a href="https://github.com/lrstanley/context7-http/actions?query=workflow%3Atest+event%3Apush">
 27 |     <img title="GitHub Workflow Status (test @ master)" src="https://img.shields.io/github/actions/workflow/status/lrstanley/context7-http/test.yml?branch=master&label=test&style=flat-square">
 28 |   </a>
 29 | 
 30 | 
 31 | 
 32 |   <a href="https://codecov.io/gh/lrstanley/context7-http">
 33 |     <img title="Code Coverage" src="https://img.shields.io/codecov/c/github/lrstanley/context7-http/master?style=flat-square">
 34 |   </a>
 35 | 
 36 |   <a href="https://pkg.go.dev/github.com/lrstanley/context7-http">
 37 |     <img title="Go Documentation" src="https://pkg.go.dev/badge/github.com/lrstanley/context7-http?style=flat-square">
 38 |   </a>
 39 |   <a href="https://goreportcard.com/report/github.com/lrstanley/context7-http">
 40 |     <img title="Go Report Card" src="https://goreportcard.com/badge/github.com/lrstanley/context7-http?style=flat-square">
 41 |   </a>
 42 | </p>
 43 | <p align="center">
 44 |   <a href="https://github.com/lrstanley/context7-http/issues?q=is:open+is:issue+label:bug">
 45 |     <img title="Bug reports" src="https://img.shields.io/github/issues/lrstanley/context7-http/bug?label=issues&style=flat-square">
 46 |   </a>
 47 |   <a href="https://github.com/lrstanley/context7-http/issues?q=is:open+is:issue+label:enhancement">
 48 |     <img title="Feature requests" src="https://img.shields.io/github/issues/lrstanley/context7-http/enhancement?label=feature%20requests&style=flat-square">
 49 |   </a>
 50 |   <a href="https://github.com/lrstanley/context7-http/pulls">
 51 |     <img title="Open Pull Requests" src="https://img.shields.io/github/issues-pr/lrstanley/context7-http?label=prs&style=flat-square">
 52 |   </a>
 53 |   <a href="https://github.com/lrstanley/context7-http/releases">
 54 |     <img title="Latest Semver Release" src="https://img.shields.io/github/v/release/lrstanley/context7-http?style=flat-square">
 55 |     <img title="Latest Release Date" src="https://img.shields.io/github/release-date/lrstanley/context7-http?label=date&style=flat-square">
 56 |   </a>
 57 |   <a href="https://github.com/lrstanley/context7-http/discussions/new?category=q-a">
 58 |     <img title="Ask a Question" src="https://img.shields.io/badge/support-ask_a_question!-blue?style=flat-square">
 59 |   </a>
 60 |   <a href="https://liam.sh/chat"><img src="https://img.shields.io/badge/discord-bytecord-blue.svg?style=flat-square" title="Discord Chat"></a>
 61 | </p>
 62 | <!-- template:end:header -->
 63 | 
 64 | <!-- template:begin:toc -->
 65 | <!-- do not edit anything in this "template" block, its auto-generated -->
 66 | ## :link: Table of Contents
 67 | 
 68 |   - [Features](#sparkles-features)
 69 |   - [Usage](#gear-usage)
 70 |     - [VSCode, Cursor, etc](#vscode-cursor-etc)
 71 |     - [Install in Windsurf](#install-in-windsurf)
 72 |     - [Install in Zed](#install-in-zed)
 73 |     - [Install in Claude Code](#install-in-claude-code)
 74 |     - [Install in Claude Desktop](#install-in-claude-desktop)
 75 |     - [Install in BoltAI](#install-in-boltai)
 76 |     - [Container Images (ghcr)](#whale-container-images-ghcr)
 77 |   - [References](#books-references)
 78 |   - [Support &amp; Assistance](#raising_hand_man-support--assistance)
 79 |   - [Contributing](#handshake-contributing)
 80 |   - [License](#balance_scale-license)
 81 | <!-- template:end:toc -->
 82 | 
 83 | ## :sparkles: Features
 84 | 
 85 | **context7-http** is a MCP server that supports HTTP streaming for the [Context7](https://context7.com) project.
 86 | This allows you to utilize the MCP server from anywhere, without installing anything locally.
 87 | 
 88 | - Has _current_ feature parity with the existing Context7 MCP Server.
 89 | - SSE and HTTP `streamable` support.
 90 | - Provides `resolve-library-uri` and `search-library-docs` tools for finding libraries, and searching their documentation.
 91 | - Provides multiple resources, including:
 92 |   - `context7://libraries` - returns high-level information about all libraries.
 93 |   - `context7://libraries/<project>` (**TODO**: not fully functional in upstream SDK)
 94 |   - `context7://libraries/top/<n>` - returns the top `n` libraries, sorted by trust score (if available), otherwise by stars.
 95 | - Currently utilizing [mcp-go](https://github.com/mark3labs/mcp-go), however, will be replaced with the official Go MCP sdk in the future.
 96 | ---
 97 | 
 98 | ## :gear: Usage
 99 | 
100 | If you'd like to run context7-http yourself, use the following:
101 | 
102 | ```console
103 | $ context7-http \
104 |     --debug \
105 |     --bind-addr "0.0.0.0:8080" \
106 |     --base-url https://context7.your-domain.com \ # only needed if using sse, http streamable doesn't need this
107 |     --trusted-proxies "x-forwarded-for,10.0.0.0/8" \ # if behind a reverse proxy
108 |     --heartbeat-interval 55s # for those with spotty networks or annoying network proxies
109 | ```
110 | 
111 | ------------
112 | 
113 | For all examples below, replace `context7.liam.sh` with your own MCP server URL, if you're running your own instance.
114 | 
115 | Configured endpoints:
116 | 
117 | - `https://context7.liam.sh/mcp` - HTTP `streamable` endpoint.
118 | - `https://context7.liam.sh/sse` (& `/message`) - SSE endpoint (**NOTE**: SSE is considered deprecated in the MCP spec).
119 | 
120 | Other than swapping out the `mcpServer` block (or similar, depending on your client), usage should match that of the
121 | [official Context7 documentation](https://github.com/upstash/context7#-with-context7)
122 | 
123 | ### VSCode, Cursor, etc
124 | 
125 | [Cursor MCP docs](https://docs.cursor.com/context/model-context-protocol#configuring-mcp-servers), and
126 | [VS Code MCP docs](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) for more info.
127 | 
128 | ```json
129 | {
130 |   "mcpServers": {
131 |     "context7": {
132 |       "url": "https://context7.liam.sh/mcp"
133 |     }
134 |   }
135 | }
136 | ```
137 | 
138 | ### Install in Windsurf
139 | 
140 | Add this to your Windsurf MCP config file. See [Windsurf MCP docs](https://docs.windsurf.com/windsurf/cascade/mcp#mcp-config-json) for more info.
141 | 
142 | ```json
143 | {
144 |   "mcpServers": {
145 |     "context7": {
146 |       "url": "https://context7.liam.sh/mcp"
147 |     }
148 |   }
149 | }
150 | ```
151 | 
152 | ### Install in Zed
153 | 
154 | Add this to your Zed `settings.json`. See [Zed MCP docs](https://zed.dev/docs/ai/mcp#bring-your-own-mcp-server) for more info.
155 | 
156 | ```json
157 | {
158 |   "context_servers": {
159 |     "context7": {
160 |       "url": "https://context7.liam.sh/mcp"
161 |     }
162 |   }
163 | }
164 | ```
165 | 
166 | ### Install in Claude Code
167 | 
168 | Run this command. See [Claude Code MCP docs](https://docs.anthropic.com/en/docs/claude-code/tutorials#configure-mcp-servers) for more info.
169 | 
170 | ```sh
171 | claude mcp add --transport sse context7 https://context7.liam.sh/sse
172 | ```
173 | 
174 | ### Install in Claude Desktop
175 | 
176 | Add this to your Claude Desktop `claude_desktop_config.json` file. See [Claude Desktop MCP docs](https://modelcontextprotocol.io/quickstart/user) for more info.
177 | 
178 | ```json
179 | {
180 |   "mcpServers": {
181 |     "context7": {
182 |       "url": "https://context7.liam.sh/mcp"
183 |     }
184 |   }
185 | }
186 | ```
187 | 
188 | ### Install in BoltAI
189 | 
190 | [BoltAI MCP docs](https://docs.boltai.com/docs/plugins/mcp-servers#how-to-use-an-mcp-server-in-boltai).
191 | 
192 | <!-- template:begin:ghcr -->
193 | <!-- do not edit anything in this "template" block, its auto-generated -->
194 | ### :whale: Container Images (ghcr)
195 | 
196 | ```console
197 | $ docker run -it --rm ghcr.io/lrstanley/context7-http:master
198 | $ docker run -it --rm ghcr.io/lrstanley/context7-http:0.3.0
199 | $ docker run -it --rm ghcr.io/lrstanley/context7-http:latest
200 | $ docker run -it --rm ghcr.io/lrstanley/context7-http:0.2.0
201 | $ docker run -it --rm ghcr.io/lrstanley/context7-http:0.1.0
202 | ```
203 | <!-- template:end:ghcr -->
204 | 
205 | ## :books: References
206 | 
207 | - [Context7](https://context7.com) - [repo](https://github.com/upstash/context7)
208 | - [Model Context Protocol Introduction](https://modelcontextprotocol.io/introduction)
209 | 
210 | ---
211 | 
212 | <!-- template:begin:support -->
213 | <!-- do not edit anything in this "template" block, its auto-generated -->
214 | ## :raising_hand_man: Support & Assistance
215 | 
216 | * :heart: Please review the [Code of Conduct](.github/CODE_OF_CONDUCT.md) for
217 |      guidelines on ensuring everyone has the best experience interacting with
218 |      the community.
219 | * :raising_hand_man: Take a look at the [support](.github/SUPPORT.md) document on
220 |      guidelines for tips on how to ask the right questions.
221 | * :lady_beetle: For all features/bugs/issues/questions/etc, [head over here](https://github.com/lrstanley/context7-http/issues/new/choose).
222 | <!-- template:end:support -->
223 | 
224 | <!-- template:begin:contributing -->
225 | <!-- do not edit anything in this "template" block, its auto-generated -->
226 | ## :handshake: Contributing
227 | 
228 | * :heart: Please review the [Code of Conduct](.github/CODE_OF_CONDUCT.md) for guidelines
229 |      on ensuring everyone has the best experience interacting with the
230 |     community.
231 | * :clipboard: Please review the [contributing](.github/CONTRIBUTING.md) doc for submitting
232 |      issues/a guide on submitting pull requests and helping out.
233 | * :old_key: For anything security related, please review this repositories [security policy](https://github.com/lrstanley/context7-http/security/policy).
234 | <!-- template:end:contributing -->
235 | 
236 | <!-- template:begin:license -->
237 | <!-- do not edit anything in this "template" block, its auto-generated -->
238 | ## :balance_scale: License
239 | 
240 | ```
241 | MIT License
242 | 
243 | Copyright (c) 2025 Liam Stanley <[email protected]>
244 | 
245 | Permission is hereby granted, free of charge, to any person obtaining a copy
246 | of this software and associated documentation files (the "Software"), to deal
247 | in the Software without restriction, including without limitation the rights
248 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
249 | copies of the Software, and to permit persons to whom the Software is
250 | furnished to do so, subject to the following conditions:
251 | 
252 | The above copyright notice and this permission notice shall be included in all
253 | copies or substantial portions of the Software.
254 | 
255 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
256 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
257 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
258 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
259 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
260 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
261 | SOFTWARE.
262 | ```
263 | 
264 | _Also located [here](LICENSE)_
265 | <!-- template:end:license -->
266 | 
```

--------------------------------------------------------------------------------
/.github/SECURITY.md:
--------------------------------------------------------------------------------

```markdown
 1 | <!-- THIS FILE IS GENERATED! DO NOT EDIT! Maintained by Terraform. -->
 2 | # :old_key: Security Policy
 3 | 
 4 | ## :heavy_check_mark: Supported Versions
 5 | 
 6 | The following restrictions apply for versions that are still supported in terms of security and bug fixes:
 7 | 
 8 |    * :grey_question: Must be using the latest major/minor version.
 9 |    * :grey_question: Must be using a supported platform for the repository (e.g. OS, browser, etc), and that platform must
10 |      be within its supported versions (for example: don't use a legacy or unsupported version of Ubuntu or
11 |      Google Chrome).
12 |    * :grey_question: Repository must not be archived (unless the vulnerability is critical, and the repository moderately
13 |      popular).
14 |    * :heavy_check_mark:
15 | 
16 | If one of the above doesn't apply to you, feel free to submit an issue and we can discuss the
17 | issue/vulnerability further.
18 | 
19 | 
20 | ## :lady_beetle: Reporting a Vulnerability
21 | 
22 | Best method of contact: [GPG :key:](https://github.com/lrstanley.gpg)
23 | 
24 |    * :speech_balloon: [Discord][chat]: message `lrstanley` (`/home/liam#0000`).
25 |    * :email: Email: `[email protected]`
26 | 
27 | Backup contacts (if I am unresponsive after **48h**): [GPG :key:](https://github.com/FM1337.gpg)
28 |    * :speech_balloon: [Discord][chat]: message `Allen#7440`.
29 |    * :email: Email: `[email protected]`
30 | 
31 | If you feel that this disclosure doesn't include a critical vulnerability and there is no sensitive
32 | information in the disclosure, you don't have to use the GPG key. For all other situations, please
33 | use it.
34 | 
35 | ### :stopwatch: Vulnerability disclosure expectations
36 | 
37 |    * :no_bell: We expect you to not share this information with others, unless:
38 |        * The maximum timeline for initial response has been exceeded (shown below).
39 |        * The maximum resolution time has been exceeded (shown below).
40 |    * :mag_right: We expect you to responsibly investigate this vulnerability -- please do not utilize the
41 |      vulnerability beyond the initial findings.
42 |    * :stopwatch: Initial response within 48h, however, if the primary contact shown above is unavailable, please
43 |      use the backup contacts provided. The maximum timeline for an initial response should be within
44 |      7 days.
45 |    * :stopwatch: Depending on the severity of the disclosure, resolution time may be anywhere from 24h to 2
46 |      weeks after initial response, though in most cases it will likely be closer to the former.
47 |        * If the vulnerability is very low/low in terms of risk, the above timelines **will not apply**.
48 |    * :toolbox: Before the release of resolved versions, a [GitHub Security Advisory][advisory-docs].
49 |      will be released on the respective repository. [Browser all advisories here][advisory].
50 | 
51 | <!-- definitions -->
52 | [chat]: https://liam.sh/chat
53 | [advisory]: https://github.com/advisories?query=type%3Areviewed+ecosystem%3Ago
54 | [advisory-docs]: https://docs.github.com/en/code-security/repository-security-advisories/creating-a-repository-security-advisory
55 | 
```

--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------

```markdown
  1 | <!-- THIS FILE IS GENERATED! DO NOT EDIT! Maintained by Terraform. -->
  2 | # :handshake: Contributing
  3 | 
  4 | This document outlines some of the guidelines that we try and adhere to while
  5 | working on this project.
  6 | 
  7 | > :point_right: **Note**: before participating in the community, please read our
  8 | > [Code of Conduct][coc].
  9 | > By interacting with this repository, organization, or community you agree to
 10 | > abide by our Code of Conduct.
 11 | >
 12 | > Additionally, if you contribute **any source code** to this repository, you
 13 | > agree to the terms of the [Developer Certificate of Origin][dco]. This helps
 14 | > ensure that contributions aren't in violation of 3rd party license terms.
 15 | 
 16 | ## :lady_beetle: Issue submission
 17 | 
 18 | When [submitting an issue][issues] or bug report,
 19 | please follow these guidelines:
 20 | 
 21 |    * Provide as much information as possible (logs, metrics, screenshots,
 22 |      runtime environment, etc).
 23 |    * Ensure that you are running on the latest stable version (tagged), or
 24 |      when using `master`, provide the specific commit being used.
 25 |    * Provide the minimum needed viable source to replicate the problem.
 26 | 
 27 | ## :bulb: Feature requests
 28 | 
 29 | When [submitting a feature request][issues], please
 30 | follow these guidelines:
 31 | 
 32 |    * Does this feature benefit others? or just your usecase? If the latter,
 33 |      it will likely be declined, unless it has a more broad benefit to others.
 34 |    * Please include the pros and cons of the feature.
 35 |    * If possible, describe how the feature would work, and any diagrams/mock
 36 |      examples of what the feature would look like.
 37 | 
 38 | ## :rocket: Pull requests
 39 | 
 40 | To review what is currently being worked on, or looked into, feel free to head
 41 | over to the [open pull requests][pull-requests] or [issues list][issues].
 42 | 
 43 | ## :raised_back_of_hand: Assistance with discussions
 44 | 
 45 |    * Take a look at the [open discussions][discussions], and if you feel like
 46 |      you'd like to help out other members of the community, it would be much
 47 |      appreciated!
 48 | 
 49 | ## :pushpin: Guidelines
 50 | 
 51 | ### :test_tube: Language agnostic
 52 | 
 53 | Below are a few guidelines if you would like to contribute:
 54 | 
 55 |    * If the feature is large or the bugfix has potential breaking changes,
 56 |      please open an issue first to ensure the changes go down the best path.
 57 |    * If possible, break the changes into smaller PRs. Pull requests should be
 58 |      focused on a specific feature/fix.
 59 |    * Pull requests will only be accepted with sufficient documentation
 60 |      describing the new functionality/fixes.
 61 |    * Keep the code simple where possible. Code that is smaller/more compact
 62 |      does not mean better. Don't do magic behind the scenes.
 63 |    * Use the same formatting/styling/structure as existing code.
 64 |    * Follow idioms and community-best-practices of the related language,
 65 |      unless the previous above guidelines override what the community
 66 |      recommends.
 67 |    * Always test your changes, both the features/fixes being implemented, but
 68 |      also in the standard way that a user would use the project (not just
 69 |      your configuration that fixes your issue).
 70 |    * Only use 3rd party libraries when necessary. If only a small portion of
 71 |      the library is needed, simply rewrite it within the library to prevent
 72 |      useless imports.
 73 | 
 74 | ### :hamster: Golang
 75 | 
 76 |    * See [golang/go/wiki/CodeReviewComments](https://github.com/golang/go/wiki/CodeReviewComments)
 77 |    * This project uses [golangci-lint](https://golangci-lint.run/) for
 78 |      Go-related files. This should be available for any editor that supports
 79 |      `gopls`, however you can also run it locally with `golangci-lint run`
 80 |      after installing it.
 81 | 
 82 | 
 83 | 
 84 | 
 85 | 
 86 | 
 87 | 
 88 | 
 89 | 
 90 | ## :clipboard: References
 91 | 
 92 |    * [Open Source: How to Contribute](https://opensource.guide/how-to-contribute/)
 93 |    * [About pull requests](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests)
 94 |    * [GitHub Docs](https://docs.github.com/)
 95 | 
 96 | ## :speech_balloon: What to do next?
 97 | 
 98 |    * :old_key: Find a vulnerability? Check out our [Security and Disclosure][security] policy.
 99 |    * :link: Repository [License][license].
100 |    * [Support][support]
101 |    * [Code of Conduct][coc].
102 | 
103 | <!-- definitions -->
104 | [coc]: https://github.com/lrstanley/context7-http/blob/master/.github/CODE_OF_CONDUCT.md
105 | [dco]: https://developercertificate.org/
106 | [discussions]: https://github.com/lrstanley/context7-http/discussions
107 | [issues]: https://github.com/lrstanley/context7-http/issues/new/choose
108 | [license]: https://github.com/lrstanley/context7-http/blob/master/LICENSE
109 | [pull-requests]: https://github.com/lrstanley/context7-http/pulls?q=is%3Aopen+is%3Apr
110 | [security]: https://github.com/lrstanley/context7-http/security/policy
111 | [support]: https://github.com/lrstanley/context7-http/blob/master/.github/SUPPORT.md
112 | 
```

--------------------------------------------------------------------------------
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------

```markdown
  1 | <!-- THIS FILE IS GENERATED! DO NOT EDIT! Maintained by Terraform. -->
  2 | # Code of Conduct
  3 | 
  4 | ## Our Pledge :purple_heart:
  5 | 
  6 | We as members, contributors, and leaders pledge to make participation in our
  7 | community a harassment-free experience for everyone, regardless of age, body
  8 | size, visible or invisible disability, ethnicity, sex characteristics, gender
  9 | identity and expression, level of experience, education, socio-economic status,
 10 | nationality, personal appearance, race, caste, color, religion, or sexual
 11 | identity and orientation.
 12 | 
 13 | We pledge to act and interact in ways that contribute to an open, welcoming,
 14 | diverse, inclusive, and healthy community.
 15 | 
 16 | ## Our Standards
 17 | 
 18 | Examples of behavior that contributes to a positive environment for our
 19 | community include:
 20 | 
 21 | * Demonstrating empathy and kindness toward other people
 22 | * Being respectful of differing opinions, viewpoints, and experiences
 23 | * Giving and gracefully accepting constructive feedback
 24 | * Accepting responsibility and apologizing to those affected by our mistakes,
 25 |   and learning from the experience
 26 | * Focusing on what is best not just for us as individuals, but for the overall
 27 |   community
 28 | 
 29 | Examples of unacceptable behavior include:
 30 | 
 31 | * The use of sexualized language or imagery, and sexual attention or advances of
 32 |   any kind
 33 | * Trolling, insulting or derogatory comments, and personal or political attacks
 34 | * Public or private harassment
 35 | * Publishing others' private information, such as a physical or email address,
 36 |   without their explicit permission
 37 | * Other conduct which could reasonably be considered inappropriate in a
 38 |   professional setting
 39 | 
 40 | ## Enforcement Responsibilities
 41 | 
 42 | Community leaders are responsible for clarifying and enforcing our standards of
 43 | acceptable behavior and will take appropriate and fair corrective action in
 44 | response to any behavior that they deem inappropriate, threatening, offensive,
 45 | or harmful.
 46 | 
 47 | Community leaders have the right and responsibility to remove, edit, or reject
 48 | comments, commits, code, wiki edits, issues, and other contributions that are
 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation
 50 | decisions when appropriate.
 51 | 
 52 | ## Scope
 53 | 
 54 | This Code of Conduct applies within all community spaces, and also applies when
 55 | an individual is officially representing the community in public spaces.
 56 | Examples of representing our community include using an official e-mail address,
 57 | posting via an official social media account, or acting as an appointed
 58 | representative at an online or offline event.
 59 | 
 60 | ## Enforcement
 61 | 
 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
 63 | reported to the community leaders responsible for enforcement at
 64 | [email protected]. All complaints will be reviewed and investigated
 65 | promptly and fairly.
 66 | 
 67 | All community leaders are obligated to respect the privacy and security of the
 68 | reporter of any incident.
 69 | 
 70 | ## Enforcement Guidelines
 71 | 
 72 | Community leaders will follow these Community Impact Guidelines in determining
 73 | the consequences for any action they deem in violation of this Code of Conduct:
 74 | 
 75 | ### 1. Correction
 76 | 
 77 | **Community Impact**: Use of inappropriate language or other behavior deemed
 78 | unprofessional or unwelcome in the community.
 79 | 
 80 | **Consequence**: A private, written warning from community leaders, providing
 81 | clarity around the nature of the violation and an explanation of why the
 82 | behavior was inappropriate. A public apology may be requested.
 83 | 
 84 | ### 2. Warning
 85 | 
 86 | **Community Impact**: A violation through a single incident or series of
 87 | actions.
 88 | 
 89 | **Consequence**: A warning with consequences for continued behavior. No
 90 | interaction with the people involved, including unsolicited interaction with
 91 | those enforcing the Code of Conduct, for a specified period of time. This
 92 | includes avoiding interactions in community spaces as well as external channels
 93 | like social media. Violating these terms may lead to a temporary or permanent
 94 | ban.
 95 | 
 96 | ### 3. Temporary Ban
 97 | 
 98 | **Community Impact**: A serious violation of community standards, including
 99 | sustained inappropriate behavior.
100 | 
101 | **Consequence**: A temporary ban from any sort of interaction or public
102 | communication with the community for a specified period of time. No public or
103 | private interaction with the people involved, including unsolicited interaction
104 | with those enforcing the Code of Conduct, is allowed during this period.
105 | Violating these terms may lead to a permanent ban.
106 | 
107 | ### 4. Permanent Ban
108 | 
109 | **Community Impact**: Demonstrating a pattern of violation of community
110 | standards, including sustained inappropriate behavior, harassment of an
111 | individual, or aggression toward or disparagement of classes of individuals.
112 | 
113 | **Consequence**: A permanent ban from any sort of public interaction within the
114 | community.
115 | 
116 | ## Attribution
117 | 
118 | This Code of Conduct is adapted from the Contributor Covenant,
119 | version 2.1, available [here](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html).
120 | 
121 | For answers to common questions about this code of conduct, see the [FAQ](https://www.contributor-covenant.org/faq).
122 | Translations are available at [translations](https://www.contributor-covenant.org/translations).
123 | 
```

--------------------------------------------------------------------------------
/.github/workflows/docker-prune.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: docker-prune
 2 | 
 3 | on:
 4 |   workflow_dispatch: {}
 5 |   schedule:
 6 |     - cron: "0 2 * * *"
 7 | 
 8 | jobs:
 9 |   docker-prune:
10 |     uses: lrstanley/.github/.github/workflows/docker-prune.yml@master
11 |     secrets: inherit
12 | 
```

--------------------------------------------------------------------------------
/.github/workflows/generate-readme.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: generate-readme
 2 | 
 3 | on:
 4 |   push:
 5 |     branches: [master]
 6 |     tags: [v*]
 7 |   schedule:
 8 |     - cron: "0 13 * * *"
 9 | 
10 | jobs:
11 |   generate:
12 |     uses: lrstanley/.github/.github/workflows/generate-readme.yml@master
13 |     secrets: inherit
14 | 
```

--------------------------------------------------------------------------------
/.github/workflows/triage.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: triage
 2 | 
 3 | on:
 4 |   pull_request_target:
 5 |     types: [opened, edited, reopened, synchronize]
 6 |   issues:
 7 |     types: [opened, edited, closed, reopened]
 8 |   issue_comment:
 9 |     types: [created, edited]
10 | 
11 | jobs:
12 |   triage:
13 |     uses: lrstanley/.github/.github/workflows/triage.yml@master
14 |     secrets: inherit
15 | 
```

--------------------------------------------------------------------------------
/.github/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | # syntax = docker/dockerfile:1.4
 2 | 
 3 | # backend
 4 | FROM golang:latest as build
 5 | 
 6 | COPY . /build
 7 | WORKDIR /build
 8 | RUN \
 9 | 	--mount=type=cache,target=/root/.cache \
10 | 	--mount=type=cache,target=/go \
11 | 	make build
12 | 
13 | # runtime
14 | FROM alpine:3.21
15 | 
16 | RUN apk add --no-cache ca-certificates
17 | COPY --from=build /build/context7-http /app/context7-http
18 | 
19 | EXPOSE 8080
20 | WORKDIR /app
21 | CMD ["/app/context7-http"]
22 | 
```

--------------------------------------------------------------------------------
/.github/workflows/renovate.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: renovate
 2 | 
 3 | on:
 4 |   workflow_dispatch:
 5 |     inputs:
 6 |       force-run:
 7 |         description: >-
 8 |           Force a run regardless of the schedule configuration.
 9 |         required: false
10 |         default: false
11 |         type: boolean
12 |   push:
13 |     branches: [master]
14 |   schedule:
15 |     - cron: "0 5 1,15 * *"
16 | 
17 | jobs:
18 |   renovate:
19 |     uses: lrstanley/.github/.github/workflows/renovate.yml@master
20 |     secrets: inherit
21 |     with:
22 |       force-run: ${{ inputs.force-run == true || github.event_name == 'schedule' }}
23 | 
```

--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: release
 2 | 
 3 | on:
 4 |   push:
 5 |     branches: [master, main]
 6 |     tags: [v*]
 7 |     paths-ignore: [".gitignore", "**/*.md", ".github/ISSUE_TEMPLATE/**"]
 8 | 
 9 | jobs:
10 |   go-release:
11 |     if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
12 |     uses: lrstanley/.github/.github/workflows/lang-go-release.yml@master
13 |     secrets: inherit
14 |     with:
15 |       upload-artifacts: true
16 |   docker-release:
17 |     uses: lrstanley/.github/.github/workflows/docker-release.yml@master
18 |     secrets: inherit
19 |     with:
20 |       dockerfile: .github/Dockerfile
21 | 
```

--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | name: test
 2 | 
 3 | on:
 4 |   pull_request:
 5 |     branches: [master]
 6 |     paths-ignore: [".gitignore", "**/*.md", ".github/ISSUE_TEMPLATE/**"]
 7 |     types: [opened, edited, reopened, synchronize, unlocked]
 8 |   push:
 9 |     branches: [master]
10 |     paths-ignore: [".gitignore", "**/*.md", ".github/ISSUE_TEMPLATE/**"]
11 | 
12 | jobs:
13 |   go-test:
14 |     uses: lrstanley/.github/.github/workflows/lang-go-test-matrix.yml@master
15 |     secrets: inherit
16 |     with:
17 |       go-version: latest
18 |   go-lint:
19 |     uses: lrstanley/.github/.github/workflows/lang-go-lint.yml@master
20 |     secrets: inherit
21 | 
```

--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------

```yaml
 1 | # THIS FILE IS GENERATED! DO NOT EDIT! Maintained by Terraform.
 2 | blank_issues_enabled: false
 3 | contact_links:
 4 |   - name: "🙋‍♂️ Ask the community a question!"
 5 |     about: Have a question, that might not be a bug? Wondering how to solve a problem? Ask away!
 6 |     url: "https://github.com/lrstanley/context7-http/discussions/new?category=q-a"
 7 |   - name: "🎉 Show us what you've made!"
 8 |     about: Have you built something using context7-http, and want to show others? Post here!
 9 |     url: "https://github.com/lrstanley/context7-http/discussions/new?category=show-and-tell"
10 |   - name: "✋ Additional support information"
11 |     about: Looking for something else? Check here.
12 |     url: "https://github.com/lrstanley/context7-http/blob/master/.github/SUPPORT.md"
13 |   - name: "💬 Discord chat"
14 |     about: On-topic and off-topic discussions.
15 |     url: "https://liam.sh/chat"
16 | 
```

--------------------------------------------------------------------------------
/internal/mcpserver/templates.go:
--------------------------------------------------------------------------------

```go
 1 | // Copyright (c) Liam Stanley <[email protected]>. All rights reserved. Use of
 2 | // this source code is governed by the MIT license that can be found in
 3 | // the LICENSE file.
 4 | 
 5 | package mcpserver
 6 | 
 7 | import (
 8 | 	"embed"
 9 | 	"maps"
10 | 	"strings"
11 | 	"text/template"
12 | 
13 | 	"github.com/Masterminds/sprig/v3"
14 | )
15 | 
16 | var (
17 | 	//go:embed templates
18 | 	templateDir embed.FS
19 | 
20 | 	templates = template.Must(
21 | 		template.New("base").
22 | 			Funcs(sprig.FuncMap()).
23 | 			ParseFS(templateDir, "templates/*.gotmpl"),
24 | 	)
25 | )
26 | 
27 | func (s *Server) render(name string, data map[string]any) (string, error) {
28 | 	var out strings.Builder
29 | 
30 | 	merged := maps.Clone(s.baseVariables)
31 | 	maps.Copy(merged, data)
32 | 
33 | 	err := templates.ExecuteTemplate(&out, name+".gotmpl", merged)
34 | 	if err != nil {
35 | 		return "", err
36 | 	}
37 | 	return out.String(), nil
38 | }
39 | 
40 | func (s *Server) mustRender(name string, data map[string]any) string {
41 | 	out, err := s.render(name, data)
42 | 	if err != nil {
43 | 		panic(err)
44 | 	}
45 | 	return out
46 | }
47 | 
```

--------------------------------------------------------------------------------
/.github/workflows/tag-semver.yml:
--------------------------------------------------------------------------------

```yaml
 1 | name: tag-semver
 2 | 
 3 | on:
 4 |   workflow_dispatch:
 5 |     inputs:
 6 |       method:
 7 |         description: "Tagging method to use"
 8 |         required: true
 9 |         type: choice
10 |         options: [major, minor, patch, alpha, rc, custom]
11 |       custom:
12 |         description: "Custom tag, if the default doesn't suffice. Must also use method 'custom'."
13 |         required: false
14 |         type: string
15 |       ref:
16 |         description: "Git ref to apply tag to (will use default branch if unspecified)."
17 |         required: false
18 |         type: string
19 |       annotation:
20 |         description: "Optional annotation to add to the commit."
21 |         required: false
22 |         type: string
23 | 
24 | jobs:
25 |   tag-semver:
26 |     uses: lrstanley/.github/.github/workflows/tag-semver.yml@master
27 |     secrets: inherit
28 |     with:
29 |       method: ${{ github.event.inputs.method }}
30 |       ref: ${{ github.event.inputs.ref }}
31 |       custom: ${{ github.event.inputs.custom }}
32 |       annotation: ${{ github.event.inputs.annotation }}
33 | 
```

--------------------------------------------------------------------------------
/internal/mcpserver/utils.go:
--------------------------------------------------------------------------------

```go
 1 | // Copyright (c) Liam Stanley <[email protected]>. All rights reserved. Use of
 2 | // this source code is governed by the MIT license that can be found in
 3 | // the LICENSE file.
 4 | 
 5 | package mcpserver
 6 | 
 7 | import (
 8 | 	"encoding/json"
 9 | 	"fmt"
10 | 
11 | 	"github.com/lrstanley/context7-http/internal/api"
12 | 	"github.com/mark3labs/mcp-go/mcp"
13 | )
14 | 
15 | // resourceSliceToJSON converts a slice of resources to a slice of mcp.ResourceContents,
16 | // with the MIME type set to application/json and JSON marshalled.
17 | func resourceSliceToJSON[T api.Resource](input []T) (results []mcp.ResourceContents, err error) {
18 | 	results = make([]mcp.ResourceContents, len(input))
19 | 	var data []byte
20 | 	for i, result := range input {
21 | 		data, err = json.Marshal(result)
22 | 		if err != nil {
23 | 			return results, fmt.Errorf("failed to marshal library to json: %w", err)
24 | 		}
25 | 
26 | 		results[i] = mcp.TextResourceContents{
27 | 			URI:      result.GetResourceURI(),
28 | 			MIMEType: "application/json",
29 | 			Text:     string(data),
30 | 		}
31 | 	}
32 | 	return results, nil
33 | }
34 | 
```

--------------------------------------------------------------------------------
/internal/mcpserver/server.go:
--------------------------------------------------------------------------------

```go
 1 | // Copyright (c) Liam Stanley <[email protected]>. All rights reserved. Use of
 2 | // this source code is governed by the MIT license that can be found in
 3 | // the LICENSE file.
 4 | 
 5 | package mcpserver
 6 | 
 7 | import (
 8 | 	"context"
 9 | 
10 | 	"github.com/lrstanley/context7-http/internal/api"
11 | 	"github.com/mark3labs/mcp-go/server"
12 | )
13 | 
14 | type Server struct {
15 | 	*server.MCPServer
16 | 	client        *api.Client
17 | 	baseVariables map[string]any
18 | }
19 | 
20 | func New(_ context.Context, version string, client *api.Client) (*Server, error) {
21 | 	name := "Context7"
22 | 	srv := &Server{
23 | 		client: client,
24 | 		MCPServer: server.NewMCPServer(
25 | 			name,
26 | 			version,
27 | 			server.WithRecovery(),
28 | 			server.WithToolCapabilities(false),
29 | 			server.WithHooks(loggingHooks(nil)),
30 | 			server.WithPaginationLimit(250),
31 | 		),
32 | 	}
33 | 
34 | 	srv.AddTool(srv.toolResolveLibraryID())
35 | 	srv.AddTool(srv.toolSearchLibraryDocs())
36 | 	srv.AddResource(srv.resourceLibrariesAll())
37 | 	srv.AddResource(srv.resourceLibrariesTop(500))
38 | 	srv.AddResource(srv.resourceLibrariesTop(1000))
39 | 	// Non-functional at this time.
40 | 	// srv.AddResourceTemplate(srv.resourceLibrary())
41 | 
42 | 	srv.baseVariables = map[string]any{
43 | 		"ServerName":    name,
44 | 		"ServerVersion": version,
45 | 	}
46 | 
47 | 	return srv, nil
48 | }
49 | 
```

--------------------------------------------------------------------------------
/internal/mcpserver/tool_resolve_library_id.go:
--------------------------------------------------------------------------------

```go
 1 | // Copyright (c) Liam Stanley <[email protected]>. All rights reserved. Use of
 2 | // this source code is governed by the MIT license that can be found in
 3 | // the LICENSE file.
 4 | 
 5 | package mcpserver
 6 | 
 7 | import (
 8 | 	"context"
 9 | 
10 | 	"github.com/apex/log"
11 | 	"github.com/mark3labs/mcp-go/mcp"
12 | 	"github.com/mark3labs/mcp-go/server"
13 | )
14 | 
15 | type ResolveLibraryIDParams struct {
16 | 	LibraryName string `json:"libraryName"`
17 | }
18 | 
19 | func (s *Server) toolResolveLibraryID() (tool mcp.Tool, handler server.ToolHandlerFunc) {
20 | 	tool = mcp.NewTool(
21 | 		"resolve-library-uri",
22 | 		mcp.WithString(
23 | 			"libraryName",
24 | 			mcp.Required(),
25 | 			mcp.MinLength(2),
26 | 			mcp.MaxLength(100),
27 | 			mcp.Description("Library name to search for, returning a context7-compatible library resource URI."),
28 | 		),
29 | 		mcp.WithDescription(s.mustRender("resolve_library_id_desc", nil)),
30 | 	)
31 | 
32 | 	return tool, mcp.NewTypedToolHandler(func(ctx context.Context, _ mcp.CallToolRequest, params ResolveLibraryIDParams) (*mcp.CallToolResult, error) {
33 | 		results, err := s.client.SearchLibraries(ctx, params.LibraryName)
34 | 		if err != nil {
35 | 			log.FromContext(ctx).WithError(err).Error("failed to retrieve library documentation data from Context7")
36 | 			return mcp.NewToolResultError("Failed to retrieve library documentation data from Context7."), nil
37 | 		}
38 | 
39 | 		if len(results) == 0 {
40 | 			return mcp.NewToolResultError("No documentation libraries available matching that criteria."), nil
41 | 		}
42 | 		resp, err := s.render("resolve_library_id_resp", map[string]any{
43 | 			"Results": results,
44 | 		})
45 | 		return mcp.NewToolResultText(resp), err
46 | 	})
47 | }
48 | 
```

--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------

```go
 1 | // Copyright (c) Liam Stanley <[email protected]>. All rights reserved. Use of
 2 | // this source code is governed by the MIT license that can be found in
 3 | // the LICENSE file.
 4 | 
 5 | package main
 6 | 
 7 | import (
 8 | 	"context"
 9 | 	"time"
10 | 
11 | 	"github.com/apex/log"
12 | 	"github.com/lrstanley/chix"
13 | 	"github.com/lrstanley/clix"
14 | 	"github.com/lrstanley/context7-http/internal/api"
15 | 	"github.com/lrstanley/context7-http/internal/mcpserver"
16 | )
17 | 
18 | var (
19 | 	version = "master"
20 | 	commit  = "latest"
21 | 	date    = "-"
22 | 
23 | 	cli = &clix.CLI[Flags]{
24 | 		Links: clix.GithubLinks("github.com/lrstanley/context7-http", "master", "https://liam.sh"),
25 | 		VersionInfo: &clix.VersionInfo[Flags]{
26 | 			Version: version,
27 | 			Commit:  commit,
28 | 			Date:    date,
29 | 		},
30 | 	}
31 | 
32 | 	srv    *mcpserver.Server
33 | 	client *api.Client
34 | )
35 | 
36 | type Flags struct {
37 | 	BindAddr          string        `long:"bind-addr"          env:"BIND_ADDR" default:":8080"`
38 | 	BaseURL           string        `long:"base-url"           env:"BASE_URL" default:"http://localhost:8080"`
39 | 	TrustedProxies    []string      `long:"trusted-proxies"    env:"TRUSTED_PROXIES" env-delim:"," description:"CIDR ranges that we trust the X-Forwarded-For header from"`
40 | 	HeartbeatInterval time.Duration `long:"heartbeat-interval" env:"HEARTBEAT_INTERVAL"`
41 | }
42 | 
43 | func main() {
44 | 	cli.LoggerConfig.Pretty = true
45 | 	cli.Parse()
46 | 
47 | 	ctx := context.Background()
48 | 
49 | 	var err error
50 | 
51 | 	client, err = api.New(ctx, nil)
52 | 	if err != nil {
53 | 		log.WithError(err).Fatal("failed to initialize api client")
54 | 	}
55 | 
56 | 	srv, err = mcpserver.New(ctx, version, client)
57 | 	if err != nil {
58 | 		log.WithError(err).Fatal("failed to initialize mcp server")
59 | 	}
60 | 
61 | 	chix.SetServerDefaults = false
62 | 	if err = chix.RunContext(ctx, httpServer(ctx)); err != nil {
63 | 		log.FromContext(ctx).WithError(err).Fatal("shutting down")
64 | 	}
65 | }
66 | 
```

--------------------------------------------------------------------------------
/internal/mcpserver/logging.go:
--------------------------------------------------------------------------------

```go
 1 | // Copyright (c) Liam Stanley <[email protected]>. All rights reserved. Use of
 2 | // this source code is governed by the MIT license that can be found in
 3 | // the LICENSE file.
 4 | 
 5 | package mcpserver
 6 | 
 7 | import (
 8 | 	"context"
 9 | 	"fmt"
10 | 
11 | 	"github.com/apex/log"
12 | 	"github.com/mark3labs/mcp-go/mcp"
13 | 	"github.com/mark3labs/mcp-go/server"
14 | )
15 | 
16 | func loggingHooks(hooks *server.Hooks) *server.Hooks {
17 | 	if hooks == nil {
18 | 		hooks = &server.Hooks{}
19 | 	}
20 | 
21 | 	hooks.AddBeforeAny(func(ctx context.Context, id any, method mcp.MCPMethod, message any) {
22 | 		fields := log.Fields{
23 | 			"id":     id,
24 | 			"method": method,
25 | 			"op":     "before-any",
26 | 		}
27 | 
28 | 		switch m := message.(type) {
29 | 		case *mcp.ReadResourceRequest:
30 | 			fields["resource"] = m.Params.URI
31 | 		case *mcp.CallToolRequest:
32 | 			fields["tool"] = m.Params.Name
33 | 		default:
34 | 			fields["type"] = fmt.Sprintf("%T", m)
35 | 		}
36 | 
37 | 		log.FromContext(ctx).WithFields(fields).Debug("received event")
38 | 	})
39 | 	hooks.AddOnSuccess(func(ctx context.Context, id any, method mcp.MCPMethod, _, _ any) {
40 | 		log.FromContext(ctx).WithFields(log.Fields{
41 | 			"id":     id,
42 | 			"method": method,
43 | 			"op":     "success",
44 | 		}).Debug("received event")
45 | 	})
46 | 	hooks.AddOnError(func(ctx context.Context, id any, method mcp.MCPMethod, _ any, err error) {
47 | 		log.FromContext(ctx).WithFields(log.Fields{
48 | 			"id":     id,
49 | 			"method": method,
50 | 			"op":     "error",
51 | 		}).WithError(err).Error("error occurred")
52 | 	})
53 | 	hooks.AddBeforeInitialize(func(ctx context.Context, id any, message *mcp.InitializeRequest) {
54 | 		log.FromContext(ctx).WithFields(log.Fields{
55 | 			"id":            id,
56 | 			"method":        message.Method,
57 | 			"op":            "before-initialize",
58 | 			"proto":         message.Params.ProtocolVersion,
59 | 			"clientName":    message.Params.ClientInfo.Name,
60 | 			"clientVersion": message.Params.ClientInfo.Version,
61 | 		}).Debug("received event")
62 | 	})
63 | 
64 | 	return hooks
65 | }
66 | 
```

--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------

```markdown
 1 | <!--
 2 |   🙏 Thanks for submitting a pull request to context7-http! Please make sure to read our
 3 |   Contributing Guidelines, and Code of Conduct.
 4 | 
 5 |   ❌ You can remove any sections of this template that are not applicable to your PR.
 6 | -->
 7 | 
 8 | ## 🚀 Changes proposed by this PR
 9 | 
10 | <!-- REQUIRED:
11 |   Please include a summary of the change and which issue is fixed, or what feature is
12 |   implemented. Include relevant motivation and context.
13 | -->
14 | 
15 | 
16 | ### 🔗 Related bug reports/feature requests
17 | 
18 | <!--
19 |   Does this PR relate to any bug reports and/or feature requests? Some examples:
20 |   ❌ Remove section if there are no existing requests and/or issues.
21 | -->
22 | - fixes #(issue)
23 | - closes #(issue)
24 | - relates to #(issue)
25 | - implements #(feature)
26 | 
27 | ### 🧰 Type of change
28 | 
29 | <!-- REQUIRED: Please mark which one applies to this change, ❌ remove the others. -->
30 | - [ ] Bug fix (non-breaking change which fixes an issue).
31 | - [ ] New feature (non-breaking change which adds functionality).
32 | - [ ] Breaking change (fix or feature that causes existing functionality to not work as expected).
33 | - [ ] This change requires (or is) a documentation update.
34 | 
35 | ### 📝 Notes to reviewer
36 | 
37 | <!--
38 |   If needed, leave any special pointers for reviewing or testing your PR. If necessary,
39 |   include things like screenshots (where beneficial), to help demonstrate the changes.
40 | -->
41 | 
42 | ### 🤝 Requirements
43 | 
44 | - [ ] ✍ I have read and agree to this projects [Code of Conduct](../../blob/master/.github/CODE_OF_CONDUCT.md).
45 | - [ ] ✍ I have read and agree to this projects [Contribution Guidelines](../../blob/master/.github/CONTRIBUTING.md).
46 | - [ ] ✍ I have read and agree to the [Developer Certificate of Origin](https://developercertificate.org/).
47 | - [ ] 🔎 I have performed a self-review of my own changes.
48 | - [ ] 🎨 My changes follow the style guidelines of this project.
49 | <!-- Include the below if this is a code change, if just documentation, ❌ remove this section. -->
50 | - [ ] 💬 My changes as properly commented, primarily for hard-to-understand areas.
51 | - [ ] 📝 I have made corresponding changes to the documentation.
52 | - [ ] 🧪 I have included tests (if necessary) for this change.
53 | 
```

--------------------------------------------------------------------------------
/internal/api/request.go:
--------------------------------------------------------------------------------

```go
 1 | // Copyright (c) Liam Stanley <[email protected]>. All rights reserved. Use of
 2 | // this source code is governed by the MIT license that can be found in
 3 | // the LICENSE file.
 4 | 
 5 | package api
 6 | 
 7 | import (
 8 | 	"context"
 9 | 	"encoding/json"
10 | 	"fmt"
11 | 	"io"
12 | 	"net/http"
13 | 	"time"
14 | 
15 | 	"github.com/apex/log"
16 | )
17 | 
18 | // request is a generic function that makes an HTTP request to the given path, with
19 | // the given method, params, and body. If the type of T is a string, the body will be
20 | // read and returned as a string, otherwise [request] will attempt to parse the body
21 | // as JSON.
22 | func request[T any](
23 | 	ctx context.Context,
24 | 	client *Client,
25 | 	method,
26 | 	path string,
27 | 	params map[string]string,
28 | 	body io.Reader,
29 | ) (T, error) {
30 | 	var result T
31 | 
32 | 	req, err := http.NewRequestWithContext(ctx, method, context7BaseURL+path, body)
33 | 	if err != nil {
34 | 		return result, fmt.Errorf("failed to initialize request: %w", err)
35 | 	}
36 | 
37 | 	// To match that of the node client.
38 | 	req.Header.Set("User-Agent", "node")
39 | 	req.Header.Set("X-Context7-Source", "mcp-server")
40 | 
41 | 	if params != nil {
42 | 		query := req.URL.Query()
43 | 		for k, v := range params {
44 | 			query.Set(k, v)
45 | 		}
46 | 		req.URL.RawQuery = query.Encode()
47 | 	}
48 | 
49 | 	logger := log.FromContext(ctx).WithFields(log.Fields{
50 | 		"method": req.Method,
51 | 		"url":    req.URL.String(),
52 | 	})
53 | 
54 | 	logger.Info("sending request")
55 | 	start := time.Now()
56 | 	resp, err := client.HTTPClient.Do(req)
57 | 	if err != nil {
58 | 		return result, err
59 | 	}
60 | 	defer resp.Body.Close() //nolint:errcheck
61 | 
62 | 	logger = logger.WithFields(log.Fields{
63 | 		"status":   resp.Status,
64 | 		"duration": time.Since(start).Round(time.Millisecond),
65 | 	})
66 | 
67 | 	if resp.StatusCode >= 400 {
68 | 		logger.Error("request failed")
69 | 		return result, fmt.Errorf("request failed with status code %d", resp.StatusCode)
70 | 	}
71 | 	logger.Info("request completed")
72 | 
73 | 	// json decode and wrap in generics. if type of T is string, return the body as a string.
74 | 	if _, ok := any(result).(string); ok {
75 | 		body, err := io.ReadAll(resp.Body)
76 | 		if err != nil {
77 | 			return result, err
78 | 		}
79 | 		result = any(string(body)).(T)
80 | 	} else if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
81 | 		return result, err
82 | 	}
83 | 	return result, nil
84 | }
85 | 
```

--------------------------------------------------------------------------------
/internal/mcpserver/tool_search_library_docs.go:
--------------------------------------------------------------------------------

```go
 1 | // Copyright (c) Liam Stanley <[email protected]>. All rights reserved. Use of
 2 | // this source code is governed by the MIT license that can be found in
 3 | // the LICENSE file.
 4 | 
 5 | package mcpserver
 6 | 
 7 | import (
 8 | 	"context"
 9 | 	"fmt"
10 | 
11 | 	"github.com/apex/log"
12 | 	"github.com/lrstanley/context7-http/internal/api"
13 | 	"github.com/mark3labs/mcp-go/mcp"
14 | 	"github.com/mark3labs/mcp-go/server"
15 | )
16 | 
17 | type SearchLibraryDocsParams struct {
18 | 	ResourceURI string   `json:"resourceURI"`
19 | 	Topic       string   `json:"topic,omitempty"`
20 | 	Tokens      int      `json:"tokens,omitempty"`
21 | 	Folders     []string `json:"folders,omitempty"`
22 | }
23 | 
24 | func (s *Server) toolSearchLibraryDocs() (tool mcp.Tool, handler server.ToolHandlerFunc) {
25 | 	tool = mcp.NewTool(
26 | 		"search-library-docs",
27 | 		mcp.WithString(
28 | 			"resourceURI",
29 | 			mcp.Required(),
30 | 			mcp.Description("Library resource URI (e.g., 'context7://libraries/<project>'), which is retrieved from 'resolve-library-uri'."),
31 | 		),
32 | 		mcp.WithString(
33 | 			"topic",
34 | 			mcp.Description(
35 | 				"Documentation topic to focus search on (e.g., 'hooks', 'routing')."+
36 | 					" This should be concise and specific, 1-10 words if possible."+
37 | 					" This is strongly encouraged to be provided if folders are not provided.",
38 | 			),
39 | 		),
40 | 		mcp.WithNumber(
41 | 			"tokens",
42 | 			mcp.Description(
43 | 				(fmt.Sprintf("Maximum number of tokens of documentation to retrieve (default: %d).", api.DefaultMinimumDocTokens))+
44 | 					" Higher values provide more context but consume more tokens.",
45 | 			),
46 | 		),
47 | 		mcp.WithArray(
48 | 			"folders",
49 | 			mcp.Description("List of folders to focus documentation on."),
50 | 		),
51 | 		mcp.WithDescription(s.mustRender("search_library_docs_desc", nil)),
52 | 	)
53 | 
54 | 	return tool, mcp.NewTypedToolHandler(func(ctx context.Context, _ mcp.CallToolRequest, params SearchLibraryDocsParams) (*mcp.CallToolResult, error) {
55 | 		result, err := s.client.SearchLibraryDocsText(ctx, params.ResourceURI, &api.SearchLibraryDocsParams{
56 | 			Topic:   params.Topic,
57 | 			Tokens:  params.Tokens,
58 | 			Folders: params.Folders,
59 | 		})
60 | 		if err != nil {
61 | 			log.FromContext(ctx).WithError(err).Error("failed to retrieve library documentation text from Context7")
62 | 			return mcp.NewToolResultError("Failed to retrieve library documentation text from Context7."), nil
63 | 		}
64 | 		return mcp.NewToolResultText(result), nil
65 | 	})
66 | }
67 | 
```

--------------------------------------------------------------------------------
/internal/mcpserver/resource_libraries.go:
--------------------------------------------------------------------------------

```go
 1 | // Copyright (c) Liam Stanley <[email protected]>. All rights reserved. Use of
 2 | // this source code is governed by the MIT license that can be found in
 3 | // the LICENSE file.
 4 | 
 5 | package mcpserver
 6 | 
 7 | import (
 8 | 	"context"
 9 | 	"fmt"
10 | 	"strconv"
11 | 
12 | 	"github.com/lrstanley/context7-http/internal/api"
13 | 	"github.com/mark3labs/mcp-go/mcp"
14 | 	"github.com/mark3labs/mcp-go/server"
15 | )
16 | 
17 | func (s *Server) resourceLibrariesAll() (resource mcp.Resource, handler server.ResourceHandlerFunc) {
18 | 	resource = mcp.NewResource(
19 | 		"context7://libraries",
20 | 		"get-libraries-all",
21 | 		mcp.WithMIMEType("application/json"),
22 | 		mcp.WithResourceDescription("Lists all known and tracked libraries."),
23 | 	)
24 | 
25 | 	return resource, func(ctx context.Context, _ mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
26 | 		libraries, err := s.client.ListLibraries(ctx)
27 | 		if err != nil {
28 | 			return nil, fmt.Errorf("failed to list libraries: %w", err)
29 | 		}
30 | 		return resourceSliceToJSON(libraries)
31 | 	}
32 | }
33 | 
34 | func (s *Server) resourceLibrariesTop(top int) (resource mcp.Resource, handler server.ResourceHandlerFunc) {
35 | 	resource = mcp.NewResource(
36 | 		"context7://libraries/top/"+strconv.Itoa(top),
37 | 		"get-libraries-top-"+strconv.Itoa(top),
38 | 		mcp.WithMIMEType("application/json"),
39 | 		mcp.WithResourceDescription("Lists top "+strconv.Itoa(top)+" libraries, sorted by trust score (if available), otherwise by stars."),
40 | 	)
41 | 
42 | 	return resource, func(ctx context.Context, _ mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
43 | 		libraries, err := s.client.ListTopLibraries(ctx, top)
44 | 		if err != nil {
45 | 			return nil, fmt.Errorf("failed to list top %d libraries: %w", top, err)
46 | 		}
47 | 		return resourceSliceToJSON(libraries)
48 | 	}
49 | }
50 | 
51 | func (s *Server) resourceLibrary() (template mcp.ResourceTemplate, handler server.ResourceTemplateHandlerFunc) {
52 | 	template = mcp.NewResourceTemplate(
53 | 		"context7://libraries/{project}",
54 | 		"get-library-info",
55 | 		mcp.WithTemplateMIMEType("application/json"),
56 | 		mcp.WithTemplateDescription("Retrieves information about a specific library."),
57 | 	)
58 | 
59 | 	return template, func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
60 | 		library, err := s.client.GetLibrary(ctx, request.Params.URI)
61 | 		if err != nil {
62 | 			return nil, fmt.Errorf("failed to get library: %w", err)
63 | 		}
64 | 		return resourceSliceToJSON([]*api.Library{library})
65 | 	}
66 | }
67 | 
```

--------------------------------------------------------------------------------
/.github/SUPPORT.md:
--------------------------------------------------------------------------------

```markdown
 1 | # :raising_hand_man: Support
 2 | 
 3 | This document explains where and how to get help with most of my projects.
 4 | Please ensure you read through it thoroughly.
 5 | 
 6 | > :point_right: **Note**: before participating in the community, please read our
 7 | > [Code of Conduct][coc].
 8 | > By interacting with this repository, organization, or community you agree to
 9 | > abide by its terms.
10 | 
11 | ## :grey_question: Asking quality questions
12 | 
13 | Questions can go to [Github Discussions][discussions] or feel free to join
14 | the Discord [here][chat].
15 | 
16 | Help me help you! Spend time framing questions and add links and resources.
17 | Spending the extra time up front can help save everyone time in the long run.
18 | Here are some tips:
19 | 
20 | * Don't fall for the [XY problem][xy].
21 | * Search to find out if a similar question has been asked or if a similar
22 |   issue/bug has been reported.
23 | * Try to define what you need help with:
24 |     * Is there something in particular you want to do?
25 |     * What problem are you encountering and what steps have you taken to try
26 |         and fix it?
27 |     * Is there a concept you don't understand?
28 | * Provide sample code, such as a [CodeSandbox][cs] or a simple snippet, if
29 |   possible.
30 | * Screenshots can help, but if there's important text such as code or error
31 |   messages in them, please also provide those.
32 | * The more time you put into asking your question, the better I and others
33 |   can help you.
34 | 
35 | ## :old_key: Security
36 | 
37 | For any security or vulnerability related disclosure, please follow the
38 | guidelines outlined in our [security policy][security].
39 | 
40 | ## :handshake: Contributions
41 | 
42 | See [`CONTRIBUTING.md`][contributing] on how to contribute.
43 | 
44 | <!-- definitions -->
45 | [coc]: https://github.com/lrstanley/context7-http/blob/master/.github/CODE_OF_CONDUCT.md
46 | [contributing]: https://github.com/lrstanley/context7-http/blob/master/.github/CONTRIBUTING.md
47 | [discussions]: https://github.com/lrstanley/context7-http/discussions/categories/q-a
48 | [issues]: https://github.com/lrstanley/context7-http/issues/new/choose
49 | [license]: https://github.com/lrstanley/context7-http/blob/master/LICENSE
50 | [pull-requests]: https://github.com/lrstanley/context7-http/issues/new/choose
51 | [security]: https://github.com/lrstanley/context7-http/security/policy
52 | [support]: https://github.com/lrstanley/context7-http/blob/master/.github/SUPPORT.md
53 | 
54 | [xy]: https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem/66378#66378
55 | [chat]: https://liam.sh/chat
56 | [cs]: https://codesandbox.io
57 | 
```

--------------------------------------------------------------------------------
/internal/api/client.go:
--------------------------------------------------------------------------------

```go
 1 | // Copyright (c) Liam Stanley <[email protected]>. All rights reserved. Use of
 2 | // this source code is governed by the MIT license that can be found in
 3 | // the LICENSE file.
 4 | 
 5 | package api
 6 | 
 7 | import (
 8 | 	"context"
 9 | 	"fmt"
10 | 	"net/http"
11 | 	"net/url"
12 | 	"time"
13 | 
14 | 	cache "github.com/Code-Hex/go-generics-cache"
15 | 	"github.com/Code-Hex/go-generics-cache/policy/lfu"
16 | 	"github.com/lrstanley/chix"
17 | 	"github.com/sethvargo/go-limiter"
18 | 	"github.com/sethvargo/go-limiter/memorystore"
19 | )
20 | 
21 | const (
22 | 	context7BaseURL = "https://context7.com/api"
23 | 	maxLibraryCache = 100
24 | )
25 | 
26 | type Client struct {
27 | 	HTTPClient             *http.Client
28 | 	limiter                limiter.Store
29 | 	searchLibraryCache     *cache.Cache[string, []*SearchResult]
30 | 	searchLibraryDocsCache *cache.Cache[string, string]
31 | 	listLibrariesCache     *cache.Cache[string, []*Library]
32 | }
33 | 
34 | // New creates a new API client, with associated rate limiting and caching.
35 | func New(ctx context.Context, httpClient *http.Client) (*Client, error) {
36 | 	if httpClient == nil {
37 | 		httpClient = &http.Client{Timeout: 10 * time.Second}
38 | 	}
39 | 
40 | 	c := &Client{
41 | 		HTTPClient: httpClient,
42 | 		searchLibraryCache: cache.NewContext(
43 | 			ctx,
44 | 			cache.AsLFU[string, []*SearchResult](lfu.WithCapacity(maxLibraryCache)),
45 | 		),
46 | 		listLibrariesCache: cache.NewContext(
47 | 			ctx,
48 | 			cache.AsLFU[string, []*Library](lfu.WithCapacity(maxLibraryCache)),
49 | 		),
50 | 		searchLibraryDocsCache: cache.NewContext(
51 | 			ctx,
52 | 			cache.AsLFU[string, string](lfu.WithCapacity(maxLibraryCache)),
53 | 		),
54 | 	}
55 | 
56 | 	limiter, err := memorystore.New(&memorystore.Config{
57 | 		Tokens:   10,
58 | 		Interval: 60 * time.Second,
59 | 	})
60 | 	if err != nil {
61 | 		return nil, fmt.Errorf("failed to create limiter: %w", err)
62 | 	}
63 | 	c.limiter = limiter
64 | 
65 | 	return c, nil
66 | }
67 | 
68 | func (c *Client) checkRateLimit(ctx context.Context, namespace string) (err error) {
69 | 	ip := chix.GetContextIP(ctx)
70 | 	_, _, reset, allowed, _ := c.limiter.Take(ctx, namespace+"/"+ip.String())
71 | 	if !allowed {
72 | 		return fmt.Errorf("rate limit exceeded (reset in %s)", time.Until(time.Unix(0, int64(reset))))
73 | 	}
74 | 	return nil
75 | }
76 | 
77 | type Resource interface {
78 | 	GetResourceURI() string
79 | }
80 | 
81 | // ValidateResourceURI validates a resource URI, and optionally checks that the provided
82 | // type matches the host portion of the URI.
83 | func ValidateResourceURI(uri string, optionalType string) (*url.URL, error) {
84 | 	resource, err := url.Parse(uri)
85 | 	if err != nil {
86 | 		return nil, fmt.Errorf("failed to parse resource URI: %w", err)
87 | 	}
88 | 	if resource.Scheme != "context7" {
89 | 		return nil, fmt.Errorf("invalid resource URI scheme: %s", resource.Scheme)
90 | 	}
91 | 	if optionalType != "" {
92 | 		if resource.Host != optionalType {
93 | 			return nil, fmt.Errorf("invalid resource URI type: %s", resource.Host)
94 | 		}
95 | 	}
96 | 	return resource, nil
97 | }
98 | 
```

--------------------------------------------------------------------------------
/http.go:
--------------------------------------------------------------------------------

```go
 1 | // Copyright (c) Liam Stanley <[email protected]>. All rights reserved. Use of
 2 | // this source code is governed by the MIT license that can be found in
 3 | // the LICENSE file.
 4 | 
 5 | package main
 6 | 
 7 | import (
 8 | 	"context"
 9 | 	"fmt"
10 | 	"net/http"
11 | 	"strings"
12 | 	"time"
13 | 
14 | 	"github.com/apex/log"
15 | 	"github.com/go-chi/chi/v5"
16 | 	"github.com/go-chi/chi/v5/middleware"
17 | 	"github.com/lrstanley/chix"
18 | 	"github.com/mark3labs/mcp-go/server"
19 | )
20 | 
21 | func httpServer(ctx context.Context) *http.Server {
22 | 	chix.DefaultAPIPrefix = "/"
23 | 	r := chi.NewRouter()
24 | 
25 | 	if len(cli.Flags.TrustedProxies) > 0 {
26 | 		r.Use(chix.UseRealIPCLIOpts(cli.Flags.TrustedProxies))
27 | 	}
28 | 
29 | 	// Core middeware.
30 | 	r.Use(
31 | 		chix.UseDebug(cli.Debug),
32 | 		chix.UseContextIP,
33 | 		chix.UseStructuredLogger(log.FromContext(ctx)),
34 | 		chix.UseRecoverer,
35 | 		middleware.Maybe(middleware.StripSlashes, func(r *http.Request) bool {
36 | 			return !strings.HasPrefix(r.URL.Path, "/debug/")
37 | 		}),
38 | 		middleware.Compress(5),
39 | 		chix.UseSecurityTxt(&chix.SecurityConfig{
40 | 			ExpiresIn: 182 * 24 * time.Hour,
41 | 			Contacts: []string{
42 | 				"mailto:[email protected]",
43 | 				"https://liam.sh/chat",
44 | 				"https://github.com/lrstanley",
45 | 			},
46 | 			KeyLinks:  []string{"https://github.com/lrstanley.gpg"},
47 | 			Languages: []string{"en"},
48 | 		}),
49 | 	)
50 | 
51 | 	sseServer := server.NewSSEServer(
52 | 		srv.MCPServer,
53 | 		server.WithBaseURL(cli.Flags.BaseURL),
54 | 	)
55 | 	r.Handle("/sse", sseServer)
56 | 	r.Handle("/message", sseServer)
57 | 
58 | 	streamableServer := server.NewStreamableHTTPServer(
59 | 		srv.MCPServer,
60 | 		server.WithHeartbeatInterval(cli.Flags.HeartbeatInterval),
61 | 	)
62 | 	r.Handle("/mcp", streamableServer)
63 | 
64 | 	if cli.Debug {
65 | 		r.With(chix.UsePrivateIP).Mount("/debug", middleware.Profiler())
66 | 	}
67 | 
68 | 	r.With(middleware.ThrottleBacklog(1, 5, 5*time.Second)).Get("/healthy", func(w http.ResponseWriter, r *http.Request) {
69 | 		chix.JSON(w, r, 200, chix.M{
70 | 			"status": "ok",
71 | 		})
72 | 	})
73 | 
74 | 	r.Get("/", func(w http.ResponseWriter, _ *http.Request) {
75 | 		w.WriteHeader(http.StatusOK)
76 | 		w.Header().Set("Content-Type", "text/html")
77 | 		_, _ = w.Write([]byte(`<html><body style="background-color:#383838;"><h1 style="color:white;">Context7 MCP Server</h1><ul>`))
78 | 		for _, link := range cli.Links {
79 | 			_, _ = fmt.Fprintf(w, `<li><a style="color:white;text-transform:capitalize;" href=%q>%s</a></li>`, link.URL, link.Name)
80 | 		}
81 | 		_, _ = fmt.Fprintf(w, `<li><a style="color:white;" href=%q>SSE -- <code>%s/sse</code></a></li>`, cli.Flags.BaseURL+"/sse", cli.Flags.BaseURL)
82 | 		_, _ = fmt.Fprintf(w, `<li><a style="color:white;" href=%q>MCP -- <code>%s/mcp</code></a></li>`, cli.Flags.BaseURL+"/mcp", cli.Flags.BaseURL)
83 | 		_, _ = w.Write([]byte(`</ul></body></html>`))
84 | 	})
85 | 
86 | 	r.NotFound(func(w http.ResponseWriter, r *http.Request) {
87 | 		chix.Error(w, r, chix.WrapCode(http.StatusNotFound))
88 | 	})
89 | 
90 | 	return &http.Server{
91 | 		Addr:    cli.Flags.BindAddr,
92 | 		Handler: r,
93 | 		// Must explicitly stay set to 0 because long-lived connections.
94 | 		ReadTimeout:  0,
95 | 		WriteTimeout: 0,
96 | 	}
97 | }
98 | 
```

--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------

```yaml
 1 | # THIS FILE IS GENERATED! DO NOT EDIT! Maintained by Terraform.
 2 | name: "🐞 Submit a bug report"
 3 | description: Create a report to help us improve!
 4 | title: "bug: [REPLACE ME]"
 5 | labels:
 6 |   - bug
 7 | body:
 8 |   - type: markdown
 9 |     attributes:
10 |       value: |
11 |         ### Thanks for submitting a bug report to **context7-http**! 📋
12 | 
13 |         - 💬 Make sure to check out the [**discussions**](../discussions) section. If your issue isn't a bug (or you're not sure), and you're looking for help to solve it, please [start a discussion here](../discussions/new?category=q-a) first.
14 |         - 🔎 Please [**search**](../labels/bug) to see if someone else has submitted a similar bug report, before making a new report.
15 | 
16 |         ----------------------------------------
17 |   - type: textarea
18 |     id: description
19 |     attributes:
20 |       label: "🌧 Describe the problem"
21 |       description: A clear and concise description of what the problem is.
22 |       placeholder: 'Example: "When I attempted to do X, I got X error"'
23 |     validations:
24 |       required: true
25 |   - type: textarea
26 |     id: expected
27 |     attributes:
28 |       label: "⛅ Expected behavior"
29 |       description: A clear and concise description of what you expected to happen.
30 |       placeholder: 'Example: "I expected X to let me add Y component, and be successful"'
31 |     validations:
32 |       required: true
33 |   - type: textarea
34 |     id: reproduce
35 |     attributes:
36 |       label: "🔄 Minimal reproduction"
37 |       description: >-
38 |         Steps to reproduce the behavior (including code examples and/or
39 |         configuration files if necessary)
40 |       placeholder: >-
41 |         Example: "1. Click on '....' | 2. Run command with flags --foo --bar,
42 |         etc | 3. See error"
43 |   - type: input
44 |     id: version
45 |     attributes:
46 |       label: "💠 Version: context7-http"
47 |       description: What version of context7-http is being used?
48 |       placeholder: 'Examples: "v1.2.3, master branch, commit 1a2b3c"'
49 |     validations:
50 |       required: true
51 |   - type: dropdown
52 |     id: os
53 |     attributes:
54 |       label: "🖥 Version: Operating system"
55 |       description: >-
56 |         What operating system did this issue occur on (if other, specify in
57 |         "Additional context" section)?
58 |       options:
59 |         - linux/ubuntu
60 |         - linux/debian
61 |         - linux/centos
62 |         - linux/alpine
63 |         - linux/other
64 |         - windows/10
65 |         - windows/11
66 |         - windows/other
67 |         - macos
68 |         - other
69 |     validations:
70 |       required: true
71 |   - type: textarea
72 |     id: context
73 |     attributes:
74 |       label: "⚙ Additional context"
75 |       description: >-
76 |         Add any other context about the problem here. This includes things
77 |         like logs, screenshots, code examples, what was the state when the
78 |         bug occurred?
79 |       placeholder: >
80 |         Examples: "logs, code snippets, screenshots, os/browser version info,
81 |         etc"
82 |   - type: checkboxes
83 |     id: requirements
84 |     attributes:
85 |       label: "🤝 Requirements"
86 |       description: "Please confirm the following:"
87 |       options:
88 |         - label: >-
89 |             I believe the problem I'm facing is a bug, and is not intended
90 |             behavior. [Post here if you're not sure](../discussions/new?category=q-a).
91 |           required: true
92 |         - label: >-
93 |             I have confirmed that someone else has not
94 |             [submitted a similar bug report](../labels/bug).
95 |           required: true
96 | 
```

--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------

```yaml
 1 | # THIS FILE IS GENERATED! DO NOT EDIT! Maintained by Terraform.
 2 | name: "💡 Submit a feature request"
 3 | description: Suggest an awesome feature for this project!
 4 | title: "feature: [REPLACE ME]"
 5 | labels:
 6 |   - enhancement
 7 | body:
 8 |   - type: markdown
 9 |     attributes:
10 |       value: |
11 |         ### Thanks for submitting a feature request! 📋
12 | 
13 |         - 💬 Make sure to check out the [**discussions**](../discussions) section of this repository. Do you have an idea for an improvement, but want to brainstorm it with others first? [Start a discussion here](../discussions/new?category=ideas) first.
14 |         - 🔎 Please [**search**](../labels/enhancement) to see if someone else has submitted a similar feature request, before making a new request.
15 | 
16 |         ---------------------------------------------
17 |   - type: textarea
18 |     id: describe
19 |     attributes:
20 |       label: "✨ Describe the feature you'd like"
21 |       description: >-
22 |         A clear and concise description of what you want to happen, or what
23 |         feature you'd like added.
24 |       placeholder: 'Example: "It would be cool if X had support for Y"'
25 |     validations:
26 |       required: true
27 |   - type: textarea
28 |     id: related
29 |     attributes:
30 |       label: "🌧 Is your feature request related to a problem?"
31 |       description: >-
32 |         A clear and concise description of what the problem is.
33 |       placeholder: >-
34 |         Example: "I'd like to see X feature added, as I frequently have to do Y,
35 |         and I think Z would solve that problem"
36 |   - type: textarea
37 |     id: alternatives
38 |     attributes:
39 |       label: "🔎 Describe alternatives you've considered"
40 |       description: >-
41 |         A clear and concise description of any alternative solutions or features
42 |         you've considered.
43 |       placeholder: >-
44 |         Example: "I've considered X and Y, however the potential problems with
45 |         those solutions would be [...]"
46 |     validations:
47 |       required: true
48 |   - type: dropdown
49 |     id: breaking
50 |     attributes:
51 |       label: "⚠ If implemented, do you think this feature will be a breaking change to users?"
52 |       description: >-
53 |         To the best of your ability, do you think implementing this change
54 |         would impact users in a way during an upgrade process?
55 |       options:
56 |         - "Yes"
57 |         - "No"
58 |         - "Not sure"
59 |     validations:
60 |       required: true
61 |   - type: textarea
62 |     id: context
63 |     attributes:
64 |       label: "⚙ Additional context"
65 |       description: >-
66 |         Add any other context or screenshots about the feature request here
67 |         (attach if necessary).
68 |       placeholder: "Examples: logs, screenshots, etc"
69 |   - type: checkboxes
70 |     id: requirements
71 |     attributes:
72 |       label: "🤝 Requirements"
73 |       description: "Please confirm the following:"
74 |       options:
75 |         - label: >-
76 |             I have confirmed that someone else has not
77 |             [submitted a similar feature request](../labels/enhancement).
78 |           required: true
79 |         - label: >-
80 |             If implemented, I believe this feature will help others, in
81 |             addition to solving my problems.
82 |           required: true
83 |         - label: I have looked into alternative solutions to the best of my ability.
84 |           required: true
85 |         - label: >-
86 |             (optional) I would be willing to contribute to testing this
87 |             feature if implemented, or making a PR to implement this
88 |             functionality.
89 |           required: false
90 | 
```

--------------------------------------------------------------------------------
/internal/api/search_libraries.go:
--------------------------------------------------------------------------------

```go
  1 | // Copyright (c) Liam Stanley <[email protected]>. All rights reserved. Use of
  2 | // this source code is governed by the MIT license that can be found in
  3 | // the LICENSE file.
  4 | 
  5 | package api
  6 | 
  7 | import (
  8 | 	"context"
  9 | 	"fmt"
 10 | 	"net/http"
 11 | 	"net/url"
 12 | 	"strconv"
 13 | 	"strings"
 14 | 	"time"
 15 | 
 16 | 	cache "github.com/Code-Hex/go-generics-cache"
 17 | )
 18 | 
 19 | const DefaultMinimumDocTokens = 10000
 20 | 
 21 | type SearchResp struct {
 22 | 	Results []*SearchResult `json:"results"`
 23 | }
 24 | 
 25 | type SearchResult struct {
 26 | 	ID            string    `json:"id"`
 27 | 	Title         string    `json:"title"`
 28 | 	Description   string    `json:"description"`
 29 | 	LastUpdate    time.Time `json:"lastUpdateDate"`
 30 | 	TotalTokens   int       `json:"totalTokens"`
 31 | 	TotalSnippets int       `json:"totalSnippets"`
 32 | 	Stars         int       `json:"stars"`
 33 | 	TrustScore    float64   `json:"trustScore,omitempty"`
 34 | 
 35 | 	// Fields that we don't currently need.
 36 | 	// Branch     string `json:"branch"`
 37 | 	// State      string `json:"state"`
 38 | 	// TotalPages int    `json:"totalPages"`
 39 | 	// LastUpdate string `json:"lastUpdate"` // Date only.
 40 | }
 41 | 
 42 | func (s *SearchResult) GetResourceURI() string {
 43 | 	return "context7://libraries/" + strings.TrimLeft(s.ID, "/")
 44 | }
 45 | 
 46 | // SearchLibraries searches the Context7 API for libraries matching the given query.
 47 | // It returns a list of search results, sorted by relevance.
 48 | func (c *Client) SearchLibraries(ctx context.Context, query string) (results []*SearchResult, err error) {
 49 | 	query = strings.TrimSpace(query)
 50 | 	if query == "" {
 51 | 		return results, nil
 52 | 	}
 53 | 
 54 | 	results, ok := c.searchLibraryCache.Get(query)
 55 | 	if ok {
 56 | 		return results, nil
 57 | 	}
 58 | 
 59 | 	err = c.checkRateLimit(ctx, "search-libraries")
 60 | 	if err != nil {
 61 | 		return results, err
 62 | 	}
 63 | 
 64 | 	resp, err := request[*SearchResp](
 65 | 		ctx, c,
 66 | 		http.MethodGet,
 67 | 		"/v1/search",
 68 | 		map[string]string{"query": query},
 69 | 		http.NoBody,
 70 | 	)
 71 | 	if err != nil {
 72 | 		return results, fmt.Errorf("failed to send request: %w", err)
 73 | 	}
 74 | 
 75 | 	c.searchLibraryCache.Set(query, resp.Results, cache.WithExpiration(time.Minute*30))
 76 | 	return resp.Results, nil
 77 | }
 78 | 
 79 | type SearchLibraryDocsParams struct {
 80 | 	Topic   string   `json:"topic"`
 81 | 	Tokens  int      `json:"tokens"`
 82 | 	Folders []string `json:"folders"`
 83 | }
 84 | 
 85 | // SearchLibraryDocsText searches the Context7 API for library documentation text matching
 86 | // the given resource URI. Result is formatted as LLM-friendly text.
 87 | func (c *Client) SearchLibraryDocsText(
 88 | 	ctx context.Context,
 89 | 	resourceURI string,
 90 | 	params *SearchLibraryDocsParams,
 91 | ) (results string, err error) {
 92 | 	var resource *url.URL
 93 | 
 94 | 	resource, err = ValidateResourceURI(resourceURI, "libraries")
 95 | 	if err != nil {
 96 | 		return "", err
 97 | 	}
 98 | 
 99 | 	if params == nil {
100 | 		params = &SearchLibraryDocsParams{}
101 | 	}
102 | 	if params.Tokens == 0 {
103 | 		params.Tokens = DefaultMinimumDocTokens
104 | 	}
105 | 
106 | 	query := map[string]string{
107 | 		"type":   "txt", // Supports JSON, but unlikely we'd need at this point.
108 | 		"tokens": strconv.Itoa(params.Tokens),
109 | 	}
110 | 	if params.Topic != "" {
111 | 		query["topic"] = params.Topic
112 | 	}
113 | 	if len(params.Folders) > 0 {
114 | 		query["folders"] = strings.Join(params.Folders, ",")
115 | 	}
116 | 
117 | 	key := resource.String() + ":" + fmt.Sprintf("%v", query)
118 | 
119 | 	var ok bool
120 | 	results, ok = c.searchLibraryDocsCache.Get(key)
121 | 	if ok {
122 | 		return results, nil
123 | 	}
124 | 
125 | 	results, err = request[string](
126 | 		ctx, c,
127 | 		http.MethodGet,
128 | 		"/v1/"+resource.Path,
129 | 		query,
130 | 		http.NoBody,
131 | 	)
132 | 	if err != nil {
133 | 		return "", fmt.Errorf("failed to send request: %w", err)
134 | 	}
135 | 
136 | 	c.searchLibraryDocsCache.Set(key, results, cache.WithExpiration(time.Minute*5))
137 | 	return results, nil
138 | }
139 | 
```

--------------------------------------------------------------------------------
/internal/api/list_libraries.go:
--------------------------------------------------------------------------------

```go
  1 | // Copyright (c) Liam Stanley <[email protected]>. All rights reserved. Use of
  2 | // this source code is governed by the MIT license that can be found in
  3 | // the LICENSE file.
  4 | 
  5 | package api
  6 | 
  7 | import (
  8 | 	"context"
  9 | 	"errors"
 10 | 	"fmt"
 11 | 	"net/http"
 12 | 	"slices"
 13 | 	"strconv"
 14 | 	"strings"
 15 | 	"time"
 16 | 
 17 | 	cache "github.com/Code-Hex/go-generics-cache"
 18 | )
 19 | 
 20 | const (
 21 | 	minTopLibraries = 250
 22 | 	maxTopLibraries = 2500
 23 | )
 24 | 
 25 | type Library struct {
 26 | 	Settings *LibrarySettings `json:"settings"`
 27 | 	Version  *LibraryVersion  `json:"version"`
 28 | }
 29 | 
 30 | func (l *Library) GetResourceURI() string {
 31 | 	return "context7://libraries/" + strings.TrimLeft(l.Settings.Project, "/")
 32 | }
 33 | 
 34 | type LibraryVersion struct {
 35 | 	LastUpdateDate time.Time `json:"lastUpdateDate"`
 36 | 	TotalSnippets  int       `json:"totalSnippets"`
 37 | 	TotalTokens    int       `json:"totalTokens"`
 38 | 
 39 | 	// Fields that we don't currently need.
 40 | 	// AverageTokens      float64   `json:"averageTokens"`
 41 | 	// ErrorCount         int       `json:"errorCount"`
 42 | 	// ParseDurationMilli int       `json:"parseDuration"`
 43 | 	// ParseTimestamp     time.Time `json:"parseDate"`
 44 | 	// SHA                string    `json:"sha"`
 45 | 	// State              string    `json:"state"`
 46 | 	// TotalPages         int       `json:"totalPages"`
 47 | }
 48 | 
 49 | type LibrarySettings struct {
 50 | 	Branch         string   `json:"branch"`
 51 | 	Description    string   `json:"description"`
 52 | 	DocsRepoURL    string   `json:"docsRepoUrl"`
 53 | 	ExcludeFolders []string `json:"excludeFolders"`
 54 | 	Folders        []string `json:"folders"`
 55 | 	Project        string   `json:"project"`
 56 | 	Stars          int      `json:"stars"`
 57 | 	Title          string   `json:"title"`
 58 | 	TrustScore     float64  `json:"trustScore"`
 59 | }
 60 | 
 61 | // ListLibraries returns all known libraries.
 62 | func (c *Client) ListLibraries(ctx context.Context) (results []*Library, err error) {
 63 | 	results, ok := c.listLibrariesCache.Get("all")
 64 | 	if ok {
 65 | 		return results, nil
 66 | 	}
 67 | 
 68 | 	err = c.checkRateLimit(ctx, "list-libraries")
 69 | 	if err != nil {
 70 | 		return results, err
 71 | 	}
 72 | 
 73 | 	results, err = request[[]*Library](
 74 | 		ctx, c,
 75 | 		http.MethodGet,
 76 | 		"/libraries",
 77 | 		nil,
 78 | 		http.NoBody,
 79 | 	)
 80 | 	if err != nil {
 81 | 		return results, fmt.Errorf("failed to send request: %w", err)
 82 | 	}
 83 | 
 84 | 	c.listLibrariesCache.Set("all", results, cache.WithExpiration(time.Minute*30))
 85 | 	return results, nil
 86 | }
 87 | 
 88 | // ListTopLibraries returns the top N libraries, sorted by TrustScore (if available),
 89 | // otherwise by Stars. Minimum number of results is 50, maximum is 1000.
 90 | func (c *Client) ListTopLibraries(ctx context.Context, top int) (results []*Library, err error) {
 91 | 	top = min(max(top, minTopLibraries), maxTopLibraries)
 92 | 
 93 | 	key := "all-top-" + strconv.Itoa(top)
 94 | 
 95 | 	results, ok := c.listLibrariesCache.Get(key)
 96 | 	if ok {
 97 | 		return results, nil
 98 | 	}
 99 | 
100 | 	err = c.checkRateLimit(ctx, "list-top-libraries")
101 | 	if err != nil {
102 | 		return results, err
103 | 	}
104 | 
105 | 	libraries, err := c.ListLibraries(ctx)
106 | 	if err != nil {
107 | 		return results, err
108 | 	}
109 | 
110 | 	// Sort by trust score (if available), then stars, then title.
111 | 	slices.SortFunc(libraries, func(a, b *Library) int {
112 | 		if a.Settings.TrustScore != b.Settings.TrustScore {
113 | 			if a.Settings.TrustScore > b.Settings.TrustScore {
114 | 				return -1
115 | 			}
116 | 			return 1
117 | 		}
118 | 
119 | 		if a.Settings.Stars != b.Settings.Stars {
120 | 			if a.Settings.Stars > b.Settings.Stars {
121 | 				return -1
122 | 			}
123 | 			return 1
124 | 		}
125 | 
126 | 		if a.Settings.Title > b.Settings.Title {
127 | 			return -1
128 | 		}
129 | 
130 | 		return 1
131 | 	})
132 | 
133 | 	results = libraries[:top]
134 | 	c.listLibrariesCache.Set(key, results, cache.WithExpiration(time.Minute*30))
135 | 	return results, nil
136 | }
137 | 
138 | // GetLibrary returns a library by its resource URI.
139 | func (c *Client) GetLibrary(ctx context.Context, resourceURI string) (library *Library, err error) {
140 | 	libraries, err := c.ListLibraries(ctx)
141 | 	if err != nil {
142 | 		return nil, err
143 | 	}
144 | 	for _, library := range libraries {
145 | 		if library.GetResourceURI() == resourceURI {
146 | 			return library, nil
147 | 		}
148 | 	}
149 | 	return nil, errors.New("library not found")
150 | }
151 | 
```