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