# 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:
--------------------------------------------------------------------------------
```
*.log
~*
*.tmp
*.txt
dist
*.test
*.prof
*.conf
*.toml
.git
bin
**/.env
context7-http
```
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
*.log
~*
*.tmp
tmp
*.txt
dist
*.test
*.prof
*.conf
**/.env
**/bin/**
*.out
context7-http
```
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
```
# THIS FILE IS GENERATED! DO NOT EDIT! Maintained by Terraform.
#
# editorconfig: https://editorconfig.org/
# actual source: https://github.com/lrstanley/.github/blob/master/terraform/github-common-files/templates/.editorconfig
#
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
max_line_length = 100
[*.tf]
indent_size = 2
[*.go]
indent_style = tab
indent_size = 4
[*.md]
trim_trailing_whitespace = false
[*.{md,py,sh,yml,yaml,cjs,js,ts,vue,css}]
max_line_length = 105
[*.{yml,yaml,toml}]
indent_size = 2
[*.json]
indent_size = 2
[*.html]
max_line_length = 140
indent_size = 2
[*.{cjs,js,ts,vue,css}]
indent_size = 2
[Makefile]
indent_style = tab
[**.min.js]
indent_style = ignore
[*.bat]
indent_style = tab
```
--------------------------------------------------------------------------------
/.golangci.yaml:
--------------------------------------------------------------------------------
```yaml
# THIS FILE IS GENERATED! DO NOT EDIT! Maintained by Terraform.
#
# golangci-lint: https://golangci-lint.run/
# false-positives: https://golangci-lint.run/usage/false-positives/
# actual source: https://github.com/lrstanley/.github/blob/master/terraform/github-common-files/templates/.golangci.yml
# modified variant of: https://gist.github.com/maratori/47a4d00457a92aa426dbd48a18776322
version: "2"
formatters:
enable: [gofumpt]
issues:
max-issues-per-linter: 0
max-same-issues: 50
severity:
default: error
rules:
- linters:
- errcheck
- gocritic
severity: warning
linters:
default: none
enable:
- asasalint # checks for pass []any as any in variadic func(...any)
- asciicheck # checks that your code does not contain non-ASCII identifiers
- bidichk # checks for dangerous unicode character sequences
- bodyclose # checks whether HTTP response body is closed successfully
- canonicalheader # checks whether net/http.Header uses canonical header
- copyloopvar # detects places where loop variables are copied (Go 1.22+)
- depguard # checks if package imports are in a list of acceptable packages
- dupl # tool for code clone detection
- durationcheck # checks for two durations multiplied together
- errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases
- errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error
- errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13
- exhaustive # checks exhaustiveness of enum switch statements
- exptostd # detects functions from golang.org/x/exp/ that can be replaced by std functions
- fatcontext # detects nested contexts in loops
- forbidigo # forbids identifiers
- funlen # tool for detection of long functions
- gocheckcompilerdirectives # validates go compiler directive comments (//go:)
- gochecknoinits # checks that no init functions are present in Go code
- gochecksumtype # checks exhaustiveness on Go "sum types"
- gocognit # computes and checks the cognitive complexity of functions
- goconst # finds repeated strings that could be replaced by a constant
- gocritic # provides diagnostics that check for bugs, performance and style issues
- godot # checks if comments end in a period
- gomoddirectives # manages the use of 'replace', 'retract', and 'excludes' directives in go.mod
- goprintffuncname # checks that printf-like functions are named with f at the end
- gosec # inspects source code for security problems
- govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string
- iface # checks the incorrect use of interfaces, helping developers avoid interface pollution
- ineffassign # detects when assignments to existing variables are not used
- intrange # finds places where for loops could make use of an integer range
- loggercheck # checks key value pairs for common logger libraries (kitlog,klog,logr,zap)
- makezero # finds slice declarations with non-zero initial length
- mirror # reports wrong mirror patterns of bytes/strings usage
- misspell # [useless] finds commonly misspelled English words in comments
- musttag # enforces field tags in (un)marshaled structs
- nakedret # finds naked returns in functions greater than a specified function length
- nestif # reports deeply nested if statements
- nilerr # finds the code that returns nil even if it checks that the error is not nil
- nilnesserr # reports that it checks for err != nil, but it returns a different nil value error (powered by nilness and nilerr)
- nilnil # checks that there is no simultaneous return of nil error and an invalid value
- noctx # finds sending http request without context.Context
- nosprintfhostport # checks for misuse of Sprintf to construct a host with port in a URL
- perfsprint # checks that fmt.Sprintf can be replaced with a faster alternative
- predeclared # finds code that shadows one of Go's predeclared identifiers
- promlinter # checks Prometheus metrics naming via promlint
- reassign # checks that package variables are not reassigned
- recvcheck # checks for receiver type consistency
- revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint
- rowserrcheck # checks whether Err of rows is checked successfully
- sloglint # ensure consistent code style when using log/slog
- sqlclosecheck # checks that sql.Rows and sql.Stmt are closed
- staticcheck # is a go vet on steroids, applying a ton of static analysis checks
- testableexamples # checks if examples are testable (have an expected output)
- testifylint # checks usage of github.com/stretchr/testify
- tparallel # detects inappropriate usage of t.Parallel() method in your Go test codes
- unconvert # removes unnecessary type conversions
- unparam # reports unused function parameters
- unused # checks for unused constants, variables, functions and types
- usestdlibvars # detects the possibility to use variables/constants from the Go standard library
- usetesting # reports uses of functions with replacement inside the testing package
- wastedassign # finds wasted assignment statements
- whitespace # detects leading and trailing whitespace
settings:
gocognit:
min-complexity: 40
errcheck:
check-type-assertions: true
funlen:
lines: 150
statements: 75
ignore-comments: true
gocritic:
disabled-checks:
- whyNoLint
- hugeParam
- ifElseChain
- singleCaseSwitch
enabled-tags:
- diagnostic
- opinionated
- performance
- style
settings:
captLocal:
paramsOnly: false
underef:
skipRecvDeref: false
rangeValCopy:
sizeThreshold: 512
depguard:
rules:
"deprecated":
files: ["$all"]
deny:
- pkg: github.com/golang/protobuf
desc: Use google.golang.org/protobuf instead, see https://developers.google.com/protocol-buffers/docs/reference/go/faq#modules
- pkg: github.com/satori/go.uuid
desc: Use github.com/google/uuid instead, satori's package is not maintained
- pkg: github.com/gofrs/uuid$
desc: Use github.com/gofrs/uuid/v5 or later, it was not a go module before v5
- pkg: github.com/lrstanley/clix$
desc: Use github.com/lrstanley/clix/v2 instead
- pkg: github.com/lrstanley/chix$
desc: Use github.com/lrstanley/chix/v2 instead
- pkg: log$
desc: Use log/slog instead, see https://go.dev/blog/slog
"non-test files":
files: ["!$test"]
deny:
- pkg: math/rand$
desc: Use math/rand/v2 instead, see https://go.dev/blog/randv2
"incorrect import":
files: ["$test"]
deny:
- pkg: github.com/tj/assert$
desc: Use github.com/stretchr/testify/assert instead, see
gochecksumtype:
default-signifies-exhaustive: false
exhaustive:
check:
- switch
- map
govet:
disable:
- fieldalignment
enable-all: true
settings:
shadow:
strict: true
perfsprint:
strconcat: false
nakedret:
max-func-lines: 0
nestif:
min-complexity: 10
rowserrcheck:
packages:
- github.com/jmoiron/sqlx
sloglint:
no-global: default
context: scope
msg-style: lowercased
static-msg: true
forbidden-keys:
- time
- level
- source
staticcheck:
checks:
- all
# Incorrect or missing package comment: https://staticcheck.dev/docs/checks/#ST1000
- -ST1000
# Use consistent method receiver names: https://staticcheck.dev/docs/checks/#ST1016
- -ST1016
# Omit embedded fields from selector expression: https://staticcheck.dev/docs/checks/#QF1008
- -QF1008
# duplicate struct tags -- used commonly for things like go-flags.
- -SA5008
usetesting:
os-temp-dir: true
exclusions:
warn-unused: true
generated: lax
presets:
- common-false-positives
- std-error-handling
paths:
- ".*\\.gen\\.go$"
- ".*\\.gen_test\\.go$"
rules:
- source: "TODO"
linters: [godot]
- text: "should have a package comment"
linters: [revive]
- text: 'exported \S+ \S+ should have comment( \(or a comment on this block\))? or be unexported'
linters: [revive]
- text: 'package comment should be of the form ".+"'
source: "// ?(nolint|TODO)"
linters: [revive]
- text: 'comment on exported \S+ \S+ should be of the form ".+"'
source: "// ?(nolint|TODO)"
linters: [revive, staticcheck]
- text: 'unexported-return: exported func \S+ returns unexported type \S+ .*'
linters: [revive]
- text: "var-declaration: should drop .* from declaration of .*; it is the zero value"
linters: [revive]
- text: ".*use ALL_CAPS in Go names.*"
linters: [revive, staticcheck]
- text: '.* always receives \S+'
linters: [unparam]
- path: _test\.go
linters:
- bodyclose
- dupl
- funlen
- gocognit
- goconst
- gosec
- noctx
- wrapcheck
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
<!-- template:define:options
{
"nodescription": true
}
-->

<!-- template:begin:header -->
<!-- do not edit anything in this "template" block, its auto-generated -->
<p align="center">
<a href="https://github.com/lrstanley/context7-http/releases">
<img title="Release Downloads" src="https://img.shields.io/github/downloads/lrstanley/context7-http/total?style=flat-square">
</a>
<a href="https://github.com/lrstanley/context7-http/tags">
<img title="Latest Semver Tag" src="https://img.shields.io/github/v/tag/lrstanley/context7-http?style=flat-square">
</a>
<a href="https://github.com/lrstanley/context7-http/commits/master">
<img title="Last commit" src="https://img.shields.io/github/last-commit/lrstanley/context7-http?style=flat-square">
</a>
<a href="https://github.com/lrstanley/context7-http/actions?query=workflow%3Atest+event%3Apush">
<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">
</a>
<a href="https://codecov.io/gh/lrstanley/context7-http">
<img title="Code Coverage" src="https://img.shields.io/codecov/c/github/lrstanley/context7-http/master?style=flat-square">
</a>
<a href="https://pkg.go.dev/github.com/lrstanley/context7-http">
<img title="Go Documentation" src="https://pkg.go.dev/badge/github.com/lrstanley/context7-http?style=flat-square">
</a>
<a href="https://goreportcard.com/report/github.com/lrstanley/context7-http">
<img title="Go Report Card" src="https://goreportcard.com/badge/github.com/lrstanley/context7-http?style=flat-square">
</a>
</p>
<p align="center">
<a href="https://github.com/lrstanley/context7-http/issues?q=is:open+is:issue+label:bug">
<img title="Bug reports" src="https://img.shields.io/github/issues/lrstanley/context7-http/bug?label=issues&style=flat-square">
</a>
<a href="https://github.com/lrstanley/context7-http/issues?q=is:open+is:issue+label:enhancement">
<img title="Feature requests" src="https://img.shields.io/github/issues/lrstanley/context7-http/enhancement?label=feature%20requests&style=flat-square">
</a>
<a href="https://github.com/lrstanley/context7-http/pulls">
<img title="Open Pull Requests" src="https://img.shields.io/github/issues-pr/lrstanley/context7-http?label=prs&style=flat-square">
</a>
<a href="https://github.com/lrstanley/context7-http/releases">
<img title="Latest Semver Release" src="https://img.shields.io/github/v/release/lrstanley/context7-http?style=flat-square">
<img title="Latest Release Date" src="https://img.shields.io/github/release-date/lrstanley/context7-http?label=date&style=flat-square">
</a>
<a href="https://github.com/lrstanley/context7-http/discussions/new?category=q-a">
<img title="Ask a Question" src="https://img.shields.io/badge/support-ask_a_question!-blue?style=flat-square">
</a>
<a href="https://liam.sh/chat"><img src="https://img.shields.io/badge/discord-bytecord-blue.svg?style=flat-square" title="Discord Chat"></a>
</p>
<!-- template:end:header -->
<!-- template:begin:toc -->
<!-- do not edit anything in this "template" block, its auto-generated -->
## :link: Table of Contents
- [Features](#sparkles-features)
- [Usage](#gear-usage)
- [VSCode, Cursor, etc](#vscode-cursor-etc)
- [Install in Windsurf](#install-in-windsurf)
- [Install in Zed](#install-in-zed)
- [Install in Claude Code](#install-in-claude-code)
- [Install in Claude Desktop](#install-in-claude-desktop)
- [Install in BoltAI](#install-in-boltai)
- [Container Images (ghcr)](#whale-container-images-ghcr)
- [References](#books-references)
- [Support & Assistance](#raising_hand_man-support--assistance)
- [Contributing](#handshake-contributing)
- [License](#balance_scale-license)
<!-- template:end:toc -->
## :sparkles: Features
**context7-http** is a MCP server that supports HTTP streaming for the [Context7](https://context7.com) project.
This allows you to utilize the MCP server from anywhere, without installing anything locally.
- Has _current_ feature parity with the existing Context7 MCP Server.
- SSE and HTTP `streamable` support.
- Provides `resolve-library-uri` and `search-library-docs` tools for finding libraries, and searching their documentation.
- Provides multiple resources, including:
- `context7://libraries` - returns high-level information about all libraries.
- `context7://libraries/<project>` (**TODO**: not fully functional in upstream SDK)
- `context7://libraries/top/<n>` - returns the top `n` libraries, sorted by trust score (if available), otherwise by stars.
- Currently utilizing [mcp-go](https://github.com/mark3labs/mcp-go), however, will be replaced with the official Go MCP sdk in the future.
---
## :gear: Usage
If you'd like to run context7-http yourself, use the following:
```console
$ context7-http \
--debug \
--bind-addr "0.0.0.0:8080" \
--base-url https://context7.your-domain.com \ # only needed if using sse, http streamable doesn't need this
--trusted-proxies "x-forwarded-for,10.0.0.0/8" \ # if behind a reverse proxy
--heartbeat-interval 55s # for those with spotty networks or annoying network proxies
```
------------
For all examples below, replace `context7.liam.sh` with your own MCP server URL, if you're running your own instance.
Configured endpoints:
- `https://context7.liam.sh/mcp` - HTTP `streamable` endpoint.
- `https://context7.liam.sh/sse` (& `/message`) - SSE endpoint (**NOTE**: SSE is considered deprecated in the MCP spec).
Other than swapping out the `mcpServer` block (or similar, depending on your client), usage should match that of the
[official Context7 documentation](https://github.com/upstash/context7#-with-context7)
### VSCode, Cursor, etc
[Cursor MCP docs](https://docs.cursor.com/context/model-context-protocol#configuring-mcp-servers), and
[VS Code MCP docs](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) for more info.
```json
{
"mcpServers": {
"context7": {
"url": "https://context7.liam.sh/mcp"
}
}
}
```
### Install in Windsurf
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.
```json
{
"mcpServers": {
"context7": {
"url": "https://context7.liam.sh/mcp"
}
}
}
```
### Install in Zed
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.
```json
{
"context_servers": {
"context7": {
"url": "https://context7.liam.sh/mcp"
}
}
}
```
### Install in Claude Code
Run this command. See [Claude Code MCP docs](https://docs.anthropic.com/en/docs/claude-code/tutorials#configure-mcp-servers) for more info.
```sh
claude mcp add --transport sse context7 https://context7.liam.sh/sse
```
### Install in Claude Desktop
Add this to your Claude Desktop `claude_desktop_config.json` file. See [Claude Desktop MCP docs](https://modelcontextprotocol.io/quickstart/user) for more info.
```json
{
"mcpServers": {
"context7": {
"url": "https://context7.liam.sh/mcp"
}
}
}
```
### Install in BoltAI
[BoltAI MCP docs](https://docs.boltai.com/docs/plugins/mcp-servers#how-to-use-an-mcp-server-in-boltai).
<!-- template:begin:ghcr -->
<!-- do not edit anything in this "template" block, its auto-generated -->
### :whale: Container Images (ghcr)
```console
$ docker run -it --rm ghcr.io/lrstanley/context7-http:master
$ docker run -it --rm ghcr.io/lrstanley/context7-http:0.3.0
$ docker run -it --rm ghcr.io/lrstanley/context7-http:latest
$ docker run -it --rm ghcr.io/lrstanley/context7-http:0.2.0
$ docker run -it --rm ghcr.io/lrstanley/context7-http:0.1.0
```
<!-- template:end:ghcr -->
## :books: References
- [Context7](https://context7.com) - [repo](https://github.com/upstash/context7)
- [Model Context Protocol Introduction](https://modelcontextprotocol.io/introduction)
---
<!-- template:begin:support -->
<!-- do not edit anything in this "template" block, its auto-generated -->
## :raising_hand_man: Support & Assistance
* :heart: Please review the [Code of Conduct](.github/CODE_OF_CONDUCT.md) for
guidelines on ensuring everyone has the best experience interacting with
the community.
* :raising_hand_man: Take a look at the [support](.github/SUPPORT.md) document on
guidelines for tips on how to ask the right questions.
* :lady_beetle: For all features/bugs/issues/questions/etc, [head over here](https://github.com/lrstanley/context7-http/issues/new/choose).
<!-- template:end:support -->
<!-- template:begin:contributing -->
<!-- do not edit anything in this "template" block, its auto-generated -->
## :handshake: Contributing
* :heart: Please review the [Code of Conduct](.github/CODE_OF_CONDUCT.md) for guidelines
on ensuring everyone has the best experience interacting with the
community.
* :clipboard: Please review the [contributing](.github/CONTRIBUTING.md) doc for submitting
issues/a guide on submitting pull requests and helping out.
* :old_key: For anything security related, please review this repositories [security policy](https://github.com/lrstanley/context7-http/security/policy).
<!-- template:end:contributing -->
<!-- template:begin:license -->
<!-- do not edit anything in this "template" block, its auto-generated -->
## :balance_scale: License
```
MIT License
Copyright (c) 2025 Liam Stanley <[email protected]>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
_Also located [here](LICENSE)_
<!-- template:end:license -->
```
--------------------------------------------------------------------------------
/.github/SECURITY.md:
--------------------------------------------------------------------------------
```markdown
<!-- THIS FILE IS GENERATED! DO NOT EDIT! Maintained by Terraform. -->
# :old_key: Security Policy
## :heavy_check_mark: Supported Versions
The following restrictions apply for versions that are still supported in terms of security and bug fixes:
* :grey_question: Must be using the latest major/minor version.
* :grey_question: Must be using a supported platform for the repository (e.g. OS, browser, etc), and that platform must
be within its supported versions (for example: don't use a legacy or unsupported version of Ubuntu or
Google Chrome).
* :grey_question: Repository must not be archived (unless the vulnerability is critical, and the repository moderately
popular).
* :heavy_check_mark:
If one of the above doesn't apply to you, feel free to submit an issue and we can discuss the
issue/vulnerability further.
## :lady_beetle: Reporting a Vulnerability
Best method of contact: [GPG :key:](https://github.com/lrstanley.gpg)
* :speech_balloon: [Discord][chat]: message `lrstanley` (`/home/liam#0000`).
* :email: Email: `[email protected]`
Backup contacts (if I am unresponsive after **48h**): [GPG :key:](https://github.com/FM1337.gpg)
* :speech_balloon: [Discord][chat]: message `Allen#7440`.
* :email: Email: `[email protected]`
If you feel that this disclosure doesn't include a critical vulnerability and there is no sensitive
information in the disclosure, you don't have to use the GPG key. For all other situations, please
use it.
### :stopwatch: Vulnerability disclosure expectations
* :no_bell: We expect you to not share this information with others, unless:
* The maximum timeline for initial response has been exceeded (shown below).
* The maximum resolution time has been exceeded (shown below).
* :mag_right: We expect you to responsibly investigate this vulnerability -- please do not utilize the
vulnerability beyond the initial findings.
* :stopwatch: Initial response within 48h, however, if the primary contact shown above is unavailable, please
use the backup contacts provided. The maximum timeline for an initial response should be within
7 days.
* :stopwatch: Depending on the severity of the disclosure, resolution time may be anywhere from 24h to 2
weeks after initial response, though in most cases it will likely be closer to the former.
* If the vulnerability is very low/low in terms of risk, the above timelines **will not apply**.
* :toolbox: Before the release of resolved versions, a [GitHub Security Advisory][advisory-docs].
will be released on the respective repository. [Browser all advisories here][advisory].
<!-- definitions -->
[chat]: https://liam.sh/chat
[advisory]: https://github.com/advisories?query=type%3Areviewed+ecosystem%3Ago
[advisory-docs]: https://docs.github.com/en/code-security/repository-security-advisories/creating-a-repository-security-advisory
```
--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
```markdown
<!-- THIS FILE IS GENERATED! DO NOT EDIT! Maintained by Terraform. -->
# :handshake: Contributing
This document outlines some of the guidelines that we try and adhere to while
working on this project.
> :point_right: **Note**: before participating in the community, please read our
> [Code of Conduct][coc].
> By interacting with this repository, organization, or community you agree to
> abide by our Code of Conduct.
>
> Additionally, if you contribute **any source code** to this repository, you
> agree to the terms of the [Developer Certificate of Origin][dco]. This helps
> ensure that contributions aren't in violation of 3rd party license terms.
## :lady_beetle: Issue submission
When [submitting an issue][issues] or bug report,
please follow these guidelines:
* Provide as much information as possible (logs, metrics, screenshots,
runtime environment, etc).
* Ensure that you are running on the latest stable version (tagged), or
when using `master`, provide the specific commit being used.
* Provide the minimum needed viable source to replicate the problem.
## :bulb: Feature requests
When [submitting a feature request][issues], please
follow these guidelines:
* Does this feature benefit others? or just your usecase? If the latter,
it will likely be declined, unless it has a more broad benefit to others.
* Please include the pros and cons of the feature.
* If possible, describe how the feature would work, and any diagrams/mock
examples of what the feature would look like.
## :rocket: Pull requests
To review what is currently being worked on, or looked into, feel free to head
over to the [open pull requests][pull-requests] or [issues list][issues].
## :raised_back_of_hand: Assistance with discussions
* Take a look at the [open discussions][discussions], and if you feel like
you'd like to help out other members of the community, it would be much
appreciated!
## :pushpin: Guidelines
### :test_tube: Language agnostic
Below are a few guidelines if you would like to contribute:
* If the feature is large or the bugfix has potential breaking changes,
please open an issue first to ensure the changes go down the best path.
* If possible, break the changes into smaller PRs. Pull requests should be
focused on a specific feature/fix.
* Pull requests will only be accepted with sufficient documentation
describing the new functionality/fixes.
* Keep the code simple where possible. Code that is smaller/more compact
does not mean better. Don't do magic behind the scenes.
* Use the same formatting/styling/structure as existing code.
* Follow idioms and community-best-practices of the related language,
unless the previous above guidelines override what the community
recommends.
* Always test your changes, both the features/fixes being implemented, but
also in the standard way that a user would use the project (not just
your configuration that fixes your issue).
* Only use 3rd party libraries when necessary. If only a small portion of
the library is needed, simply rewrite it within the library to prevent
useless imports.
### :hamster: Golang
* See [golang/go/wiki/CodeReviewComments](https://github.com/golang/go/wiki/CodeReviewComments)
* This project uses [golangci-lint](https://golangci-lint.run/) for
Go-related files. This should be available for any editor that supports
`gopls`, however you can also run it locally with `golangci-lint run`
after installing it.
## :clipboard: References
* [Open Source: How to Contribute](https://opensource.guide/how-to-contribute/)
* [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)
* [GitHub Docs](https://docs.github.com/)
## :speech_balloon: What to do next?
* :old_key: Find a vulnerability? Check out our [Security and Disclosure][security] policy.
* :link: Repository [License][license].
* [Support][support]
* [Code of Conduct][coc].
<!-- definitions -->
[coc]: https://github.com/lrstanley/context7-http/blob/master/.github/CODE_OF_CONDUCT.md
[dco]: https://developercertificate.org/
[discussions]: https://github.com/lrstanley/context7-http/discussions
[issues]: https://github.com/lrstanley/context7-http/issues/new/choose
[license]: https://github.com/lrstanley/context7-http/blob/master/LICENSE
[pull-requests]: https://github.com/lrstanley/context7-http/pulls?q=is%3Aopen+is%3Apr
[security]: https://github.com/lrstanley/context7-http/security/policy
[support]: https://github.com/lrstanley/context7-http/blob/master/.github/SUPPORT.md
```
--------------------------------------------------------------------------------
/.github/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
```markdown
<!-- THIS FILE IS GENERATED! DO NOT EDIT! Maintained by Terraform. -->
# Code of Conduct
## Our Pledge :purple_heart:
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
[email protected]. All complaints will be reviewed and investigated
promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the Contributor Covenant,
version 2.1, available [here](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html).
For answers to common questions about this code of conduct, see the [FAQ](https://www.contributor-covenant.org/faq).
Translations are available at [translations](https://www.contributor-covenant.org/translations).
```
--------------------------------------------------------------------------------
/.github/workflows/docker-prune.yml:
--------------------------------------------------------------------------------
```yaml
name: docker-prune
on:
workflow_dispatch: {}
schedule:
- cron: "0 2 * * *"
jobs:
docker-prune:
uses: lrstanley/.github/.github/workflows/docker-prune.yml@master
secrets: inherit
```
--------------------------------------------------------------------------------
/.github/workflows/generate-readme.yml:
--------------------------------------------------------------------------------
```yaml
name: generate-readme
on:
push:
branches: [master]
tags: [v*]
schedule:
- cron: "0 13 * * *"
jobs:
generate:
uses: lrstanley/.github/.github/workflows/generate-readme.yml@master
secrets: inherit
```
--------------------------------------------------------------------------------
/.github/workflows/triage.yml:
--------------------------------------------------------------------------------
```yaml
name: triage
on:
pull_request_target:
types: [opened, edited, reopened, synchronize]
issues:
types: [opened, edited, closed, reopened]
issue_comment:
types: [created, edited]
jobs:
triage:
uses: lrstanley/.github/.github/workflows/triage.yml@master
secrets: inherit
```
--------------------------------------------------------------------------------
/.github/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
# syntax = docker/dockerfile:1.4
# backend
FROM golang:latest as build
COPY . /build
WORKDIR /build
RUN \
--mount=type=cache,target=/root/.cache \
--mount=type=cache,target=/go \
make build
# runtime
FROM alpine:3.21
RUN apk add --no-cache ca-certificates
COPY --from=build /build/context7-http /app/context7-http
EXPOSE 8080
WORKDIR /app
CMD ["/app/context7-http"]
```
--------------------------------------------------------------------------------
/.github/workflows/renovate.yml:
--------------------------------------------------------------------------------
```yaml
name: renovate
on:
workflow_dispatch:
inputs:
force-run:
description: >-
Force a run regardless of the schedule configuration.
required: false
default: false
type: boolean
push:
branches: [master]
schedule:
- cron: "0 5 1,15 * *"
jobs:
renovate:
uses: lrstanley/.github/.github/workflows/renovate.yml@master
secrets: inherit
with:
force-run: ${{ inputs.force-run == true || github.event_name == 'schedule' }}
```
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
```yaml
name: release
on:
push:
branches: [master, main]
tags: [v*]
paths-ignore: [".gitignore", "**/*.md", ".github/ISSUE_TEMPLATE/**"]
jobs:
go-release:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
uses: lrstanley/.github/.github/workflows/lang-go-release.yml@master
secrets: inherit
with:
upload-artifacts: true
docker-release:
uses: lrstanley/.github/.github/workflows/docker-release.yml@master
secrets: inherit
with:
dockerfile: .github/Dockerfile
```
--------------------------------------------------------------------------------
/.github/workflows/test.yaml:
--------------------------------------------------------------------------------
```yaml
name: test
on:
pull_request:
branches: [master]
paths-ignore: [".gitignore", "**/*.md", ".github/ISSUE_TEMPLATE/**"]
types: [opened, edited, reopened, synchronize, unlocked]
push:
branches: [master]
paths-ignore: [".gitignore", "**/*.md", ".github/ISSUE_TEMPLATE/**"]
jobs:
go-test:
uses: lrstanley/.github/.github/workflows/lang-go-test-matrix.yml@master
secrets: inherit
with:
go-version: latest
go-lint:
uses: lrstanley/.github/.github/workflows/lang-go-lint.yml@master
secrets: inherit
```
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
```yaml
# THIS FILE IS GENERATED! DO NOT EDIT! Maintained by Terraform.
blank_issues_enabled: false
contact_links:
- name: "🙋♂️ Ask the community a question!"
about: Have a question, that might not be a bug? Wondering how to solve a problem? Ask away!
url: "https://github.com/lrstanley/context7-http/discussions/new?category=q-a"
- name: "🎉 Show us what you've made!"
about: Have you built something using context7-http, and want to show others? Post here!
url: "https://github.com/lrstanley/context7-http/discussions/new?category=show-and-tell"
- name: "✋ Additional support information"
about: Looking for something else? Check here.
url: "https://github.com/lrstanley/context7-http/blob/master/.github/SUPPORT.md"
- name: "💬 Discord chat"
about: On-topic and off-topic discussions.
url: "https://liam.sh/chat"
```
--------------------------------------------------------------------------------
/internal/mcpserver/templates.go:
--------------------------------------------------------------------------------
```go
// Copyright (c) Liam Stanley <[email protected]>. All rights reserved. Use of
// this source code is governed by the MIT license that can be found in
// the LICENSE file.
package mcpserver
import (
"embed"
"maps"
"strings"
"text/template"
"github.com/Masterminds/sprig/v3"
)
var (
//go:embed templates
templateDir embed.FS
templates = template.Must(
template.New("base").
Funcs(sprig.FuncMap()).
ParseFS(templateDir, "templates/*.gotmpl"),
)
)
func (s *Server) render(name string, data map[string]any) (string, error) {
var out strings.Builder
merged := maps.Clone(s.baseVariables)
maps.Copy(merged, data)
err := templates.ExecuteTemplate(&out, name+".gotmpl", merged)
if err != nil {
return "", err
}
return out.String(), nil
}
func (s *Server) mustRender(name string, data map[string]any) string {
out, err := s.render(name, data)
if err != nil {
panic(err)
}
return out
}
```
--------------------------------------------------------------------------------
/.github/workflows/tag-semver.yml:
--------------------------------------------------------------------------------
```yaml
name: tag-semver
on:
workflow_dispatch:
inputs:
method:
description: "Tagging method to use"
required: true
type: choice
options: [major, minor, patch, alpha, rc, custom]
custom:
description: "Custom tag, if the default doesn't suffice. Must also use method 'custom'."
required: false
type: string
ref:
description: "Git ref to apply tag to (will use default branch if unspecified)."
required: false
type: string
annotation:
description: "Optional annotation to add to the commit."
required: false
type: string
jobs:
tag-semver:
uses: lrstanley/.github/.github/workflows/tag-semver.yml@master
secrets: inherit
with:
method: ${{ github.event.inputs.method }}
ref: ${{ github.event.inputs.ref }}
custom: ${{ github.event.inputs.custom }}
annotation: ${{ github.event.inputs.annotation }}
```
--------------------------------------------------------------------------------
/internal/mcpserver/utils.go:
--------------------------------------------------------------------------------
```go
// Copyright (c) Liam Stanley <[email protected]>. All rights reserved. Use of
// this source code is governed by the MIT license that can be found in
// the LICENSE file.
package mcpserver
import (
"encoding/json"
"fmt"
"github.com/lrstanley/context7-http/internal/api"
"github.com/mark3labs/mcp-go/mcp"
)
// resourceSliceToJSON converts a slice of resources to a slice of mcp.ResourceContents,
// with the MIME type set to application/json and JSON marshalled.
func resourceSliceToJSON[T api.Resource](input []T) (results []mcp.ResourceContents, err error) {
results = make([]mcp.ResourceContents, len(input))
var data []byte
for i, result := range input {
data, err = json.Marshal(result)
if err != nil {
return results, fmt.Errorf("failed to marshal library to json: %w", err)
}
results[i] = mcp.TextResourceContents{
URI: result.GetResourceURI(),
MIMEType: "application/json",
Text: string(data),
}
}
return results, nil
}
```
--------------------------------------------------------------------------------
/internal/mcpserver/server.go:
--------------------------------------------------------------------------------
```go
// Copyright (c) Liam Stanley <[email protected]>. All rights reserved. Use of
// this source code is governed by the MIT license that can be found in
// the LICENSE file.
package mcpserver
import (
"context"
"github.com/lrstanley/context7-http/internal/api"
"github.com/mark3labs/mcp-go/server"
)
type Server struct {
*server.MCPServer
client *api.Client
baseVariables map[string]any
}
func New(_ context.Context, version string, client *api.Client) (*Server, error) {
name := "Context7"
srv := &Server{
client: client,
MCPServer: server.NewMCPServer(
name,
version,
server.WithRecovery(),
server.WithToolCapabilities(false),
server.WithHooks(loggingHooks(nil)),
server.WithPaginationLimit(250),
),
}
srv.AddTool(srv.toolResolveLibraryID())
srv.AddTool(srv.toolSearchLibraryDocs())
srv.AddResource(srv.resourceLibrariesAll())
srv.AddResource(srv.resourceLibrariesTop(500))
srv.AddResource(srv.resourceLibrariesTop(1000))
// Non-functional at this time.
// srv.AddResourceTemplate(srv.resourceLibrary())
srv.baseVariables = map[string]any{
"ServerName": name,
"ServerVersion": version,
}
return srv, nil
}
```
--------------------------------------------------------------------------------
/internal/mcpserver/tool_resolve_library_id.go:
--------------------------------------------------------------------------------
```go
// Copyright (c) Liam Stanley <[email protected]>. All rights reserved. Use of
// this source code is governed by the MIT license that can be found in
// the LICENSE file.
package mcpserver
import (
"context"
"github.com/apex/log"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
type ResolveLibraryIDParams struct {
LibraryName string `json:"libraryName"`
}
func (s *Server) toolResolveLibraryID() (tool mcp.Tool, handler server.ToolHandlerFunc) {
tool = mcp.NewTool(
"resolve-library-uri",
mcp.WithString(
"libraryName",
mcp.Required(),
mcp.MinLength(2),
mcp.MaxLength(100),
mcp.Description("Library name to search for, returning a context7-compatible library resource URI."),
),
mcp.WithDescription(s.mustRender("resolve_library_id_desc", nil)),
)
return tool, mcp.NewTypedToolHandler(func(ctx context.Context, _ mcp.CallToolRequest, params ResolveLibraryIDParams) (*mcp.CallToolResult, error) {
results, err := s.client.SearchLibraries(ctx, params.LibraryName)
if err != nil {
log.FromContext(ctx).WithError(err).Error("failed to retrieve library documentation data from Context7")
return mcp.NewToolResultError("Failed to retrieve library documentation data from Context7."), nil
}
if len(results) == 0 {
return mcp.NewToolResultError("No documentation libraries available matching that criteria."), nil
}
resp, err := s.render("resolve_library_id_resp", map[string]any{
"Results": results,
})
return mcp.NewToolResultText(resp), err
})
}
```
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
```go
// Copyright (c) Liam Stanley <[email protected]>. All rights reserved. Use of
// this source code is governed by the MIT license that can be found in
// the LICENSE file.
package main
import (
"context"
"time"
"github.com/apex/log"
"github.com/lrstanley/chix"
"github.com/lrstanley/clix"
"github.com/lrstanley/context7-http/internal/api"
"github.com/lrstanley/context7-http/internal/mcpserver"
)
var (
version = "master"
commit = "latest"
date = "-"
cli = &clix.CLI[Flags]{
Links: clix.GithubLinks("github.com/lrstanley/context7-http", "master", "https://liam.sh"),
VersionInfo: &clix.VersionInfo[Flags]{
Version: version,
Commit: commit,
Date: date,
},
}
srv *mcpserver.Server
client *api.Client
)
type Flags struct {
BindAddr string `long:"bind-addr" env:"BIND_ADDR" default:":8080"`
BaseURL string `long:"base-url" env:"BASE_URL" default:"http://localhost:8080"`
TrustedProxies []string `long:"trusted-proxies" env:"TRUSTED_PROXIES" env-delim:"," description:"CIDR ranges that we trust the X-Forwarded-For header from"`
HeartbeatInterval time.Duration `long:"heartbeat-interval" env:"HEARTBEAT_INTERVAL"`
}
func main() {
cli.LoggerConfig.Pretty = true
cli.Parse()
ctx := context.Background()
var err error
client, err = api.New(ctx, nil)
if err != nil {
log.WithError(err).Fatal("failed to initialize api client")
}
srv, err = mcpserver.New(ctx, version, client)
if err != nil {
log.WithError(err).Fatal("failed to initialize mcp server")
}
chix.SetServerDefaults = false
if err = chix.RunContext(ctx, httpServer(ctx)); err != nil {
log.FromContext(ctx).WithError(err).Fatal("shutting down")
}
}
```
--------------------------------------------------------------------------------
/internal/mcpserver/logging.go:
--------------------------------------------------------------------------------
```go
// Copyright (c) Liam Stanley <[email protected]>. All rights reserved. Use of
// this source code is governed by the MIT license that can be found in
// the LICENSE file.
package mcpserver
import (
"context"
"fmt"
"github.com/apex/log"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
func loggingHooks(hooks *server.Hooks) *server.Hooks {
if hooks == nil {
hooks = &server.Hooks{}
}
hooks.AddBeforeAny(func(ctx context.Context, id any, method mcp.MCPMethod, message any) {
fields := log.Fields{
"id": id,
"method": method,
"op": "before-any",
}
switch m := message.(type) {
case *mcp.ReadResourceRequest:
fields["resource"] = m.Params.URI
case *mcp.CallToolRequest:
fields["tool"] = m.Params.Name
default:
fields["type"] = fmt.Sprintf("%T", m)
}
log.FromContext(ctx).WithFields(fields).Debug("received event")
})
hooks.AddOnSuccess(func(ctx context.Context, id any, method mcp.MCPMethod, _, _ any) {
log.FromContext(ctx).WithFields(log.Fields{
"id": id,
"method": method,
"op": "success",
}).Debug("received event")
})
hooks.AddOnError(func(ctx context.Context, id any, method mcp.MCPMethod, _ any, err error) {
log.FromContext(ctx).WithFields(log.Fields{
"id": id,
"method": method,
"op": "error",
}).WithError(err).Error("error occurred")
})
hooks.AddBeforeInitialize(func(ctx context.Context, id any, message *mcp.InitializeRequest) {
log.FromContext(ctx).WithFields(log.Fields{
"id": id,
"method": message.Method,
"op": "before-initialize",
"proto": message.Params.ProtocolVersion,
"clientName": message.Params.ClientInfo.Name,
"clientVersion": message.Params.ClientInfo.Version,
}).Debug("received event")
})
return hooks
}
```
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
```markdown
<!--
🙏 Thanks for submitting a pull request to context7-http! Please make sure to read our
Contributing Guidelines, and Code of Conduct.
❌ You can remove any sections of this template that are not applicable to your PR.
-->
## 🚀 Changes proposed by this PR
<!-- REQUIRED:
Please include a summary of the change and which issue is fixed, or what feature is
implemented. Include relevant motivation and context.
-->
### 🔗 Related bug reports/feature requests
<!--
Does this PR relate to any bug reports and/or feature requests? Some examples:
❌ Remove section if there are no existing requests and/or issues.
-->
- fixes #(issue)
- closes #(issue)
- relates to #(issue)
- implements #(feature)
### 🧰 Type of change
<!-- REQUIRED: Please mark which one applies to this change, ❌ remove the others. -->
- [ ] Bug fix (non-breaking change which fixes an issue).
- [ ] New feature (non-breaking change which adds functionality).
- [ ] Breaking change (fix or feature that causes existing functionality to not work as expected).
- [ ] This change requires (or is) a documentation update.
### 📝 Notes to reviewer
<!--
If needed, leave any special pointers for reviewing or testing your PR. If necessary,
include things like screenshots (where beneficial), to help demonstrate the changes.
-->
### 🤝 Requirements
- [ ] ✍ I have read and agree to this projects [Code of Conduct](../../blob/master/.github/CODE_OF_CONDUCT.md).
- [ ] ✍ I have read and agree to this projects [Contribution Guidelines](../../blob/master/.github/CONTRIBUTING.md).
- [ ] ✍ I have read and agree to the [Developer Certificate of Origin](https://developercertificate.org/).
- [ ] 🔎 I have performed a self-review of my own changes.
- [ ] 🎨 My changes follow the style guidelines of this project.
<!-- Include the below if this is a code change, if just documentation, ❌ remove this section. -->
- [ ] 💬 My changes as properly commented, primarily for hard-to-understand areas.
- [ ] 📝 I have made corresponding changes to the documentation.
- [ ] 🧪 I have included tests (if necessary) for this change.
```
--------------------------------------------------------------------------------
/internal/api/request.go:
--------------------------------------------------------------------------------
```go
// Copyright (c) Liam Stanley <[email protected]>. All rights reserved. Use of
// this source code is governed by the MIT license that can be found in
// the LICENSE file.
package api
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/apex/log"
)
// request is a generic function that makes an HTTP request to the given path, with
// the given method, params, and body. If the type of T is a string, the body will be
// read and returned as a string, otherwise [request] will attempt to parse the body
// as JSON.
func request[T any](
ctx context.Context,
client *Client,
method,
path string,
params map[string]string,
body io.Reader,
) (T, error) {
var result T
req, err := http.NewRequestWithContext(ctx, method, context7BaseURL+path, body)
if err != nil {
return result, fmt.Errorf("failed to initialize request: %w", err)
}
// To match that of the node client.
req.Header.Set("User-Agent", "node")
req.Header.Set("X-Context7-Source", "mcp-server")
if params != nil {
query := req.URL.Query()
for k, v := range params {
query.Set(k, v)
}
req.URL.RawQuery = query.Encode()
}
logger := log.FromContext(ctx).WithFields(log.Fields{
"method": req.Method,
"url": req.URL.String(),
})
logger.Info("sending request")
start := time.Now()
resp, err := client.HTTPClient.Do(req)
if err != nil {
return result, err
}
defer resp.Body.Close() //nolint:errcheck
logger = logger.WithFields(log.Fields{
"status": resp.Status,
"duration": time.Since(start).Round(time.Millisecond),
})
if resp.StatusCode >= 400 {
logger.Error("request failed")
return result, fmt.Errorf("request failed with status code %d", resp.StatusCode)
}
logger.Info("request completed")
// json decode and wrap in generics. if type of T is string, return the body as a string.
if _, ok := any(result).(string); ok {
body, err := io.ReadAll(resp.Body)
if err != nil {
return result, err
}
result = any(string(body)).(T)
} else if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return result, err
}
return result, nil
}
```
--------------------------------------------------------------------------------
/internal/mcpserver/tool_search_library_docs.go:
--------------------------------------------------------------------------------
```go
// Copyright (c) Liam Stanley <[email protected]>. All rights reserved. Use of
// this source code is governed by the MIT license that can be found in
// the LICENSE file.
package mcpserver
import (
"context"
"fmt"
"github.com/apex/log"
"github.com/lrstanley/context7-http/internal/api"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
type SearchLibraryDocsParams struct {
ResourceURI string `json:"resourceURI"`
Topic string `json:"topic,omitempty"`
Tokens int `json:"tokens,omitempty"`
Folders []string `json:"folders,omitempty"`
}
func (s *Server) toolSearchLibraryDocs() (tool mcp.Tool, handler server.ToolHandlerFunc) {
tool = mcp.NewTool(
"search-library-docs",
mcp.WithString(
"resourceURI",
mcp.Required(),
mcp.Description("Library resource URI (e.g., 'context7://libraries/<project>'), which is retrieved from 'resolve-library-uri'."),
),
mcp.WithString(
"topic",
mcp.Description(
"Documentation topic to focus search on (e.g., 'hooks', 'routing')."+
" This should be concise and specific, 1-10 words if possible."+
" This is strongly encouraged to be provided if folders are not provided.",
),
),
mcp.WithNumber(
"tokens",
mcp.Description(
(fmt.Sprintf("Maximum number of tokens of documentation to retrieve (default: %d).", api.DefaultMinimumDocTokens))+
" Higher values provide more context but consume more tokens.",
),
),
mcp.WithArray(
"folders",
mcp.Description("List of folders to focus documentation on."),
),
mcp.WithDescription(s.mustRender("search_library_docs_desc", nil)),
)
return tool, mcp.NewTypedToolHandler(func(ctx context.Context, _ mcp.CallToolRequest, params SearchLibraryDocsParams) (*mcp.CallToolResult, error) {
result, err := s.client.SearchLibraryDocsText(ctx, params.ResourceURI, &api.SearchLibraryDocsParams{
Topic: params.Topic,
Tokens: params.Tokens,
Folders: params.Folders,
})
if err != nil {
log.FromContext(ctx).WithError(err).Error("failed to retrieve library documentation text from Context7")
return mcp.NewToolResultError("Failed to retrieve library documentation text from Context7."), nil
}
return mcp.NewToolResultText(result), nil
})
}
```
--------------------------------------------------------------------------------
/internal/mcpserver/resource_libraries.go:
--------------------------------------------------------------------------------
```go
// Copyright (c) Liam Stanley <[email protected]>. All rights reserved. Use of
// this source code is governed by the MIT license that can be found in
// the LICENSE file.
package mcpserver
import (
"context"
"fmt"
"strconv"
"github.com/lrstanley/context7-http/internal/api"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
func (s *Server) resourceLibrariesAll() (resource mcp.Resource, handler server.ResourceHandlerFunc) {
resource = mcp.NewResource(
"context7://libraries",
"get-libraries-all",
mcp.WithMIMEType("application/json"),
mcp.WithResourceDescription("Lists all known and tracked libraries."),
)
return resource, func(ctx context.Context, _ mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
libraries, err := s.client.ListLibraries(ctx)
if err != nil {
return nil, fmt.Errorf("failed to list libraries: %w", err)
}
return resourceSliceToJSON(libraries)
}
}
func (s *Server) resourceLibrariesTop(top int) (resource mcp.Resource, handler server.ResourceHandlerFunc) {
resource = mcp.NewResource(
"context7://libraries/top/"+strconv.Itoa(top),
"get-libraries-top-"+strconv.Itoa(top),
mcp.WithMIMEType("application/json"),
mcp.WithResourceDescription("Lists top "+strconv.Itoa(top)+" libraries, sorted by trust score (if available), otherwise by stars."),
)
return resource, func(ctx context.Context, _ mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
libraries, err := s.client.ListTopLibraries(ctx, top)
if err != nil {
return nil, fmt.Errorf("failed to list top %d libraries: %w", top, err)
}
return resourceSliceToJSON(libraries)
}
}
func (s *Server) resourceLibrary() (template mcp.ResourceTemplate, handler server.ResourceTemplateHandlerFunc) {
template = mcp.NewResourceTemplate(
"context7://libraries/{project}",
"get-library-info",
mcp.WithTemplateMIMEType("application/json"),
mcp.WithTemplateDescription("Retrieves information about a specific library."),
)
return template, func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
library, err := s.client.GetLibrary(ctx, request.Params.URI)
if err != nil {
return nil, fmt.Errorf("failed to get library: %w", err)
}
return resourceSliceToJSON([]*api.Library{library})
}
}
```
--------------------------------------------------------------------------------
/.github/SUPPORT.md:
--------------------------------------------------------------------------------
```markdown
# :raising_hand_man: Support
This document explains where and how to get help with most of my projects.
Please ensure you read through it thoroughly.
> :point_right: **Note**: before participating in the community, please read our
> [Code of Conduct][coc].
> By interacting with this repository, organization, or community you agree to
> abide by its terms.
## :grey_question: Asking quality questions
Questions can go to [Github Discussions][discussions] or feel free to join
the Discord [here][chat].
Help me help you! Spend time framing questions and add links and resources.
Spending the extra time up front can help save everyone time in the long run.
Here are some tips:
* Don't fall for the [XY problem][xy].
* Search to find out if a similar question has been asked or if a similar
issue/bug has been reported.
* Try to define what you need help with:
* Is there something in particular you want to do?
* What problem are you encountering and what steps have you taken to try
and fix it?
* Is there a concept you don't understand?
* Provide sample code, such as a [CodeSandbox][cs] or a simple snippet, if
possible.
* Screenshots can help, but if there's important text such as code or error
messages in them, please also provide those.
* The more time you put into asking your question, the better I and others
can help you.
## :old_key: Security
For any security or vulnerability related disclosure, please follow the
guidelines outlined in our [security policy][security].
## :handshake: Contributions
See [`CONTRIBUTING.md`][contributing] on how to contribute.
<!-- definitions -->
[coc]: https://github.com/lrstanley/context7-http/blob/master/.github/CODE_OF_CONDUCT.md
[contributing]: https://github.com/lrstanley/context7-http/blob/master/.github/CONTRIBUTING.md
[discussions]: https://github.com/lrstanley/context7-http/discussions/categories/q-a
[issues]: https://github.com/lrstanley/context7-http/issues/new/choose
[license]: https://github.com/lrstanley/context7-http/blob/master/LICENSE
[pull-requests]: https://github.com/lrstanley/context7-http/issues/new/choose
[security]: https://github.com/lrstanley/context7-http/security/policy
[support]: https://github.com/lrstanley/context7-http/blob/master/.github/SUPPORT.md
[xy]: https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem/66378#66378
[chat]: https://liam.sh/chat
[cs]: https://codesandbox.io
```
--------------------------------------------------------------------------------
/internal/api/client.go:
--------------------------------------------------------------------------------
```go
// Copyright (c) Liam Stanley <[email protected]>. All rights reserved. Use of
// this source code is governed by the MIT license that can be found in
// the LICENSE file.
package api
import (
"context"
"fmt"
"net/http"
"net/url"
"time"
cache "github.com/Code-Hex/go-generics-cache"
"github.com/Code-Hex/go-generics-cache/policy/lfu"
"github.com/lrstanley/chix"
"github.com/sethvargo/go-limiter"
"github.com/sethvargo/go-limiter/memorystore"
)
const (
context7BaseURL = "https://context7.com/api"
maxLibraryCache = 100
)
type Client struct {
HTTPClient *http.Client
limiter limiter.Store
searchLibraryCache *cache.Cache[string, []*SearchResult]
searchLibraryDocsCache *cache.Cache[string, string]
listLibrariesCache *cache.Cache[string, []*Library]
}
// New creates a new API client, with associated rate limiting and caching.
func New(ctx context.Context, httpClient *http.Client) (*Client, error) {
if httpClient == nil {
httpClient = &http.Client{Timeout: 10 * time.Second}
}
c := &Client{
HTTPClient: httpClient,
searchLibraryCache: cache.NewContext(
ctx,
cache.AsLFU[string, []*SearchResult](lfu.WithCapacity(maxLibraryCache)),
),
listLibrariesCache: cache.NewContext(
ctx,
cache.AsLFU[string, []*Library](lfu.WithCapacity(maxLibraryCache)),
),
searchLibraryDocsCache: cache.NewContext(
ctx,
cache.AsLFU[string, string](lfu.WithCapacity(maxLibraryCache)),
),
}
limiter, err := memorystore.New(&memorystore.Config{
Tokens: 10,
Interval: 60 * time.Second,
})
if err != nil {
return nil, fmt.Errorf("failed to create limiter: %w", err)
}
c.limiter = limiter
return c, nil
}
func (c *Client) checkRateLimit(ctx context.Context, namespace string) (err error) {
ip := chix.GetContextIP(ctx)
_, _, reset, allowed, _ := c.limiter.Take(ctx, namespace+"/"+ip.String())
if !allowed {
return fmt.Errorf("rate limit exceeded (reset in %s)", time.Until(time.Unix(0, int64(reset))))
}
return nil
}
type Resource interface {
GetResourceURI() string
}
// ValidateResourceURI validates a resource URI, and optionally checks that the provided
// type matches the host portion of the URI.
func ValidateResourceURI(uri string, optionalType string) (*url.URL, error) {
resource, err := url.Parse(uri)
if err != nil {
return nil, fmt.Errorf("failed to parse resource URI: %w", err)
}
if resource.Scheme != "context7" {
return nil, fmt.Errorf("invalid resource URI scheme: %s", resource.Scheme)
}
if optionalType != "" {
if resource.Host != optionalType {
return nil, fmt.Errorf("invalid resource URI type: %s", resource.Host)
}
}
return resource, nil
}
```
--------------------------------------------------------------------------------
/http.go:
--------------------------------------------------------------------------------
```go
// Copyright (c) Liam Stanley <[email protected]>. All rights reserved. Use of
// this source code is governed by the MIT license that can be found in
// the LICENSE file.
package main
import (
"context"
"fmt"
"net/http"
"strings"
"time"
"github.com/apex/log"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/lrstanley/chix"
"github.com/mark3labs/mcp-go/server"
)
func httpServer(ctx context.Context) *http.Server {
chix.DefaultAPIPrefix = "/"
r := chi.NewRouter()
if len(cli.Flags.TrustedProxies) > 0 {
r.Use(chix.UseRealIPCLIOpts(cli.Flags.TrustedProxies))
}
// Core middeware.
r.Use(
chix.UseDebug(cli.Debug),
chix.UseContextIP,
chix.UseStructuredLogger(log.FromContext(ctx)),
chix.UseRecoverer,
middleware.Maybe(middleware.StripSlashes, func(r *http.Request) bool {
return !strings.HasPrefix(r.URL.Path, "/debug/")
}),
middleware.Compress(5),
chix.UseSecurityTxt(&chix.SecurityConfig{
ExpiresIn: 182 * 24 * time.Hour,
Contacts: []string{
"mailto:[email protected]",
"https://liam.sh/chat",
"https://github.com/lrstanley",
},
KeyLinks: []string{"https://github.com/lrstanley.gpg"},
Languages: []string{"en"},
}),
)
sseServer := server.NewSSEServer(
srv.MCPServer,
server.WithBaseURL(cli.Flags.BaseURL),
)
r.Handle("/sse", sseServer)
r.Handle("/message", sseServer)
streamableServer := server.NewStreamableHTTPServer(
srv.MCPServer,
server.WithHeartbeatInterval(cli.Flags.HeartbeatInterval),
)
r.Handle("/mcp", streamableServer)
if cli.Debug {
r.With(chix.UsePrivateIP).Mount("/debug", middleware.Profiler())
}
r.With(middleware.ThrottleBacklog(1, 5, 5*time.Second)).Get("/healthy", func(w http.ResponseWriter, r *http.Request) {
chix.JSON(w, r, 200, chix.M{
"status": "ok",
})
})
r.Get("/", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(`<html><body style="background-color:#383838;"><h1 style="color:white;">Context7 MCP Server</h1><ul>`))
for _, link := range cli.Links {
_, _ = fmt.Fprintf(w, `<li><a style="color:white;text-transform:capitalize;" href=%q>%s</a></li>`, link.URL, link.Name)
}
_, _ = fmt.Fprintf(w, `<li><a style="color:white;" href=%q>SSE -- <code>%s/sse</code></a></li>`, cli.Flags.BaseURL+"/sse", cli.Flags.BaseURL)
_, _ = fmt.Fprintf(w, `<li><a style="color:white;" href=%q>MCP -- <code>%s/mcp</code></a></li>`, cli.Flags.BaseURL+"/mcp", cli.Flags.BaseURL)
_, _ = w.Write([]byte(`</ul></body></html>`))
})
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
chix.Error(w, r, chix.WrapCode(http.StatusNotFound))
})
return &http.Server{
Addr: cli.Flags.BindAddr,
Handler: r,
// Must explicitly stay set to 0 because long-lived connections.
ReadTimeout: 0,
WriteTimeout: 0,
}
}
```
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
```yaml
# THIS FILE IS GENERATED! DO NOT EDIT! Maintained by Terraform.
name: "🐞 Submit a bug report"
description: Create a report to help us improve!
title: "bug: [REPLACE ME]"
labels:
- bug
body:
- type: markdown
attributes:
value: |
### Thanks for submitting a bug report to **context7-http**! 📋
- 💬 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.
- 🔎 Please [**search**](../labels/bug) to see if someone else has submitted a similar bug report, before making a new report.
----------------------------------------
- type: textarea
id: description
attributes:
label: "🌧 Describe the problem"
description: A clear and concise description of what the problem is.
placeholder: 'Example: "When I attempted to do X, I got X error"'
validations:
required: true
- type: textarea
id: expected
attributes:
label: "⛅ Expected behavior"
description: A clear and concise description of what you expected to happen.
placeholder: 'Example: "I expected X to let me add Y component, and be successful"'
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: "🔄 Minimal reproduction"
description: >-
Steps to reproduce the behavior (including code examples and/or
configuration files if necessary)
placeholder: >-
Example: "1. Click on '....' | 2. Run command with flags --foo --bar,
etc | 3. See error"
- type: input
id: version
attributes:
label: "💠 Version: context7-http"
description: What version of context7-http is being used?
placeholder: 'Examples: "v1.2.3, master branch, commit 1a2b3c"'
validations:
required: true
- type: dropdown
id: os
attributes:
label: "🖥 Version: Operating system"
description: >-
What operating system did this issue occur on (if other, specify in
"Additional context" section)?
options:
- linux/ubuntu
- linux/debian
- linux/centos
- linux/alpine
- linux/other
- windows/10
- windows/11
- windows/other
- macos
- other
validations:
required: true
- type: textarea
id: context
attributes:
label: "⚙ Additional context"
description: >-
Add any other context about the problem here. This includes things
like logs, screenshots, code examples, what was the state when the
bug occurred?
placeholder: >
Examples: "logs, code snippets, screenshots, os/browser version info,
etc"
- type: checkboxes
id: requirements
attributes:
label: "🤝 Requirements"
description: "Please confirm the following:"
options:
- label: >-
I believe the problem I'm facing is a bug, and is not intended
behavior. [Post here if you're not sure](../discussions/new?category=q-a).
required: true
- label: >-
I have confirmed that someone else has not
[submitted a similar bug report](../labels/bug).
required: true
```
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
```yaml
# THIS FILE IS GENERATED! DO NOT EDIT! Maintained by Terraform.
name: "💡 Submit a feature request"
description: Suggest an awesome feature for this project!
title: "feature: [REPLACE ME]"
labels:
- enhancement
body:
- type: markdown
attributes:
value: |
### Thanks for submitting a feature request! 📋
- 💬 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.
- 🔎 Please [**search**](../labels/enhancement) to see if someone else has submitted a similar feature request, before making a new request.
---------------------------------------------
- type: textarea
id: describe
attributes:
label: "✨ Describe the feature you'd like"
description: >-
A clear and concise description of what you want to happen, or what
feature you'd like added.
placeholder: 'Example: "It would be cool if X had support for Y"'
validations:
required: true
- type: textarea
id: related
attributes:
label: "🌧 Is your feature request related to a problem?"
description: >-
A clear and concise description of what the problem is.
placeholder: >-
Example: "I'd like to see X feature added, as I frequently have to do Y,
and I think Z would solve that problem"
- type: textarea
id: alternatives
attributes:
label: "🔎 Describe alternatives you've considered"
description: >-
A clear and concise description of any alternative solutions or features
you've considered.
placeholder: >-
Example: "I've considered X and Y, however the potential problems with
those solutions would be [...]"
validations:
required: true
- type: dropdown
id: breaking
attributes:
label: "⚠ If implemented, do you think this feature will be a breaking change to users?"
description: >-
To the best of your ability, do you think implementing this change
would impact users in a way during an upgrade process?
options:
- "Yes"
- "No"
- "Not sure"
validations:
required: true
- type: textarea
id: context
attributes:
label: "⚙ Additional context"
description: >-
Add any other context or screenshots about the feature request here
(attach if necessary).
placeholder: "Examples: logs, screenshots, etc"
- type: checkboxes
id: requirements
attributes:
label: "🤝 Requirements"
description: "Please confirm the following:"
options:
- label: >-
I have confirmed that someone else has not
[submitted a similar feature request](../labels/enhancement).
required: true
- label: >-
If implemented, I believe this feature will help others, in
addition to solving my problems.
required: true
- label: I have looked into alternative solutions to the best of my ability.
required: true
- label: >-
(optional) I would be willing to contribute to testing this
feature if implemented, or making a PR to implement this
functionality.
required: false
```
--------------------------------------------------------------------------------
/internal/api/search_libraries.go:
--------------------------------------------------------------------------------
```go
// Copyright (c) Liam Stanley <[email protected]>. All rights reserved. Use of
// this source code is governed by the MIT license that can be found in
// the LICENSE file.
package api
import (
"context"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
cache "github.com/Code-Hex/go-generics-cache"
)
const DefaultMinimumDocTokens = 10000
type SearchResp struct {
Results []*SearchResult `json:"results"`
}
type SearchResult struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
LastUpdate time.Time `json:"lastUpdateDate"`
TotalTokens int `json:"totalTokens"`
TotalSnippets int `json:"totalSnippets"`
Stars int `json:"stars"`
TrustScore float64 `json:"trustScore,omitempty"`
// Fields that we don't currently need.
// Branch string `json:"branch"`
// State string `json:"state"`
// TotalPages int `json:"totalPages"`
// LastUpdate string `json:"lastUpdate"` // Date only.
}
func (s *SearchResult) GetResourceURI() string {
return "context7://libraries/" + strings.TrimLeft(s.ID, "/")
}
// SearchLibraries searches the Context7 API for libraries matching the given query.
// It returns a list of search results, sorted by relevance.
func (c *Client) SearchLibraries(ctx context.Context, query string) (results []*SearchResult, err error) {
query = strings.TrimSpace(query)
if query == "" {
return results, nil
}
results, ok := c.searchLibraryCache.Get(query)
if ok {
return results, nil
}
err = c.checkRateLimit(ctx, "search-libraries")
if err != nil {
return results, err
}
resp, err := request[*SearchResp](
ctx, c,
http.MethodGet,
"/v1/search",
map[string]string{"query": query},
http.NoBody,
)
if err != nil {
return results, fmt.Errorf("failed to send request: %w", err)
}
c.searchLibraryCache.Set(query, resp.Results, cache.WithExpiration(time.Minute*30))
return resp.Results, nil
}
type SearchLibraryDocsParams struct {
Topic string `json:"topic"`
Tokens int `json:"tokens"`
Folders []string `json:"folders"`
}
// SearchLibraryDocsText searches the Context7 API for library documentation text matching
// the given resource URI. Result is formatted as LLM-friendly text.
func (c *Client) SearchLibraryDocsText(
ctx context.Context,
resourceURI string,
params *SearchLibraryDocsParams,
) (results string, err error) {
var resource *url.URL
resource, err = ValidateResourceURI(resourceURI, "libraries")
if err != nil {
return "", err
}
if params == nil {
params = &SearchLibraryDocsParams{}
}
if params.Tokens == 0 {
params.Tokens = DefaultMinimumDocTokens
}
query := map[string]string{
"type": "txt", // Supports JSON, but unlikely we'd need at this point.
"tokens": strconv.Itoa(params.Tokens),
}
if params.Topic != "" {
query["topic"] = params.Topic
}
if len(params.Folders) > 0 {
query["folders"] = strings.Join(params.Folders, ",")
}
key := resource.String() + ":" + fmt.Sprintf("%v", query)
var ok bool
results, ok = c.searchLibraryDocsCache.Get(key)
if ok {
return results, nil
}
results, err = request[string](
ctx, c,
http.MethodGet,
"/v1/"+resource.Path,
query,
http.NoBody,
)
if err != nil {
return "", fmt.Errorf("failed to send request: %w", err)
}
c.searchLibraryDocsCache.Set(key, results, cache.WithExpiration(time.Minute*5))
return results, nil
}
```
--------------------------------------------------------------------------------
/internal/api/list_libraries.go:
--------------------------------------------------------------------------------
```go
// Copyright (c) Liam Stanley <[email protected]>. All rights reserved. Use of
// this source code is governed by the MIT license that can be found in
// the LICENSE file.
package api
import (
"context"
"errors"
"fmt"
"net/http"
"slices"
"strconv"
"strings"
"time"
cache "github.com/Code-Hex/go-generics-cache"
)
const (
minTopLibraries = 250
maxTopLibraries = 2500
)
type Library struct {
Settings *LibrarySettings `json:"settings"`
Version *LibraryVersion `json:"version"`
}
func (l *Library) GetResourceURI() string {
return "context7://libraries/" + strings.TrimLeft(l.Settings.Project, "/")
}
type LibraryVersion struct {
LastUpdateDate time.Time `json:"lastUpdateDate"`
TotalSnippets int `json:"totalSnippets"`
TotalTokens int `json:"totalTokens"`
// Fields that we don't currently need.
// AverageTokens float64 `json:"averageTokens"`
// ErrorCount int `json:"errorCount"`
// ParseDurationMilli int `json:"parseDuration"`
// ParseTimestamp time.Time `json:"parseDate"`
// SHA string `json:"sha"`
// State string `json:"state"`
// TotalPages int `json:"totalPages"`
}
type LibrarySettings struct {
Branch string `json:"branch"`
Description string `json:"description"`
DocsRepoURL string `json:"docsRepoUrl"`
ExcludeFolders []string `json:"excludeFolders"`
Folders []string `json:"folders"`
Project string `json:"project"`
Stars int `json:"stars"`
Title string `json:"title"`
TrustScore float64 `json:"trustScore"`
}
// ListLibraries returns all known libraries.
func (c *Client) ListLibraries(ctx context.Context) (results []*Library, err error) {
results, ok := c.listLibrariesCache.Get("all")
if ok {
return results, nil
}
err = c.checkRateLimit(ctx, "list-libraries")
if err != nil {
return results, err
}
results, err = request[[]*Library](
ctx, c,
http.MethodGet,
"/libraries",
nil,
http.NoBody,
)
if err != nil {
return results, fmt.Errorf("failed to send request: %w", err)
}
c.listLibrariesCache.Set("all", results, cache.WithExpiration(time.Minute*30))
return results, nil
}
// ListTopLibraries returns the top N libraries, sorted by TrustScore (if available),
// otherwise by Stars. Minimum number of results is 50, maximum is 1000.
func (c *Client) ListTopLibraries(ctx context.Context, top int) (results []*Library, err error) {
top = min(max(top, minTopLibraries), maxTopLibraries)
key := "all-top-" + strconv.Itoa(top)
results, ok := c.listLibrariesCache.Get(key)
if ok {
return results, nil
}
err = c.checkRateLimit(ctx, "list-top-libraries")
if err != nil {
return results, err
}
libraries, err := c.ListLibraries(ctx)
if err != nil {
return results, err
}
// Sort by trust score (if available), then stars, then title.
slices.SortFunc(libraries, func(a, b *Library) int {
if a.Settings.TrustScore != b.Settings.TrustScore {
if a.Settings.TrustScore > b.Settings.TrustScore {
return -1
}
return 1
}
if a.Settings.Stars != b.Settings.Stars {
if a.Settings.Stars > b.Settings.Stars {
return -1
}
return 1
}
if a.Settings.Title > b.Settings.Title {
return -1
}
return 1
})
results = libraries[:top]
c.listLibrariesCache.Set(key, results, cache.WithExpiration(time.Minute*30))
return results, nil
}
// GetLibrary returns a library by its resource URI.
func (c *Client) GetLibrary(ctx context.Context, resourceURI string) (library *Library, err error) {
libraries, err := c.ListLibraries(ctx)
if err != nil {
return nil, err
}
for _, library := range libraries {
if library.GetResourceURI() == resourceURI {
return library, nil
}
}
return nil, errors.New("library not found")
}
```