mirror of
https://github.com/thedaviddias/mcp-llms-txt-explorer.git
synced 2025-10-19 03:17:32 +03:00
chore: first commit
This commit is contained in:
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* @thedaviddias
|
||||
128
.github/CODE_OF_CONDUCT.md
vendored
Normal file
128
.github/CODE_OF_CONDUCT.md
vendored
Normal file
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
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, 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
|
||||
haydenbleasel.com/contact.
|
||||
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][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
63
.github/CONTRIBUTING.md
vendored
Normal file
63
.github/CONTRIBUTING.md
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
# Contributing to This Project
|
||||
|
||||
Thank you for your interest in contributing! This document outlines the process for contributing to our project.
|
||||
|
||||
## Getting Started
|
||||
|
||||
There are two ways to contribute a new website to our list:
|
||||
|
||||
### Option 1: Web Interface (Recommended)
|
||||
|
||||
The easiest way to contribute is through our web interface:
|
||||
|
||||
1. Visit our website
|
||||
2. Log in with your GitHub account
|
||||
3. Submit your website through our user-friendly form
|
||||
4. Your submission will be automatically validated and processed
|
||||
|
||||
### Option 2: Manual Pull Request
|
||||
|
||||
If you prefer to contribute directly through GitHub:
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a new branch for your addition: `git checkout -b add/your-website-name`
|
||||
3. Create a new MDX file in the content/websites directory
|
||||
4. Add your website information following our template format
|
||||
5. Test your changes thoroughly
|
||||
6. Commit your changes with clear, descriptive commit messages
|
||||
7. Push to your fork
|
||||
8. Submit a Pull Request
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
- Ensure your PR addresses a specific issue or adds value to the project
|
||||
- Include a clear description of the changes
|
||||
- Keep changes focused and atomic
|
||||
- Follow existing code style and conventions
|
||||
- Include tests if applicable
|
||||
- Update documentation as needed
|
||||
|
||||
## Code Style
|
||||
|
||||
- Follow the existing code formatting in the project (ensure you have Biome installed)
|
||||
- Write clear, self-documenting code
|
||||
- Add comments only when necessary to explain complex logic
|
||||
- Use meaningful variable and function names
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
- Use the GitHub issue tracker
|
||||
- Check if the issue already exists before creating a new one
|
||||
- Provide a clear description of the issue
|
||||
- Include steps to reproduce if applicable
|
||||
- Add relevant labels
|
||||
|
||||
## Questions or Need Help?
|
||||
|
||||
Feel free to open an issue for questions or join our discussions. We're here to help!
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Please note that this project follows a Code of Conduct. By participating, you are expected to uphold this code.
|
||||
|
||||
Thank you for contributing!
|
||||
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
github: thedaviddias
|
||||
custom: ["https://thanks.dev/u/thedaviddias"]
|
||||
48
.github/ISSUE_TEMPLATE/BUG_REPORT.yml
vendored
Normal file
48
.github/ISSUE_TEMPLATE/BUG_REPORT.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Bug Report
|
||||
description: Report a bug to help us improve
|
||||
title: "[BUG] "
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to report this bug!
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: A clear and concise description of the bug
|
||||
placeholder: When I click X, Y happens instead of Z
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: How can we reproduce this issue?
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '...'
|
||||
3. See error
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment
|
||||
description: Where does this occur?
|
||||
options:
|
||||
- Development
|
||||
- Staging
|
||||
- Production
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context, screenshots, or error messages
|
||||
48
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml
vendored
Normal file
48
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Feature Request
|
||||
description: Suggest an idea for this project
|
||||
title: "[FEATURE] "
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to suggest a new feature!
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem Statement
|
||||
description: What problem are you trying to solve?
|
||||
placeholder: I'm always frustrated when...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Proposed Solution
|
||||
description: What solution would you like to see?
|
||||
placeholder: It would be great if...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: scope
|
||||
attributes:
|
||||
label: Scope
|
||||
description: Which part of the project does this affect?
|
||||
options:
|
||||
- Frontend
|
||||
- Backend
|
||||
- Infrastructure
|
||||
- Documentation
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives Considered
|
||||
description: What alternative solutions have you considered?
|
||||
placeholder: I also thought about...
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Questions & Discussions
|
||||
url: https://github.com/thedaviddias/ai-templates/discussions
|
||||
about: Please ask and answer questions here
|
||||
7
.github/SECURITY.md
vendored
Normal file
7
.github/SECURITY.md
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you believe you have found a security vulnerability in Links Base, we encourage you to responsibly disclose this and not open a public issue. Please report it using [GitHub Security Advisory](https://github.com/thedaviddias/llms-txt-hub/security/advisories/new) tool, to ensure confidentiality and security.
|
||||
|
||||
We'll review it as soon as possible and publish a fix accordingly.
|
||||
35
.github/actions/install/action.yml
vendored
Normal file
35
.github/actions/install/action.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: Setup project dependencies
|
||||
|
||||
description: "Setup project dependencies"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install pnpm
|
||||
uses: pnpm/action-setup@v4.0.0
|
||||
id: pnpm-install
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Cache dependencies
|
||||
id: cache_dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.pnpm
|
||||
${{ github.workspace }}/.next/cache
|
||||
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
# if: steps.cache_dependencies.outputs.cache-hit != 'true'
|
||||
run: pnpm install
|
||||
0
.github/labeler.yml
vendored
Normal file
0
.github/labeler.yml
vendored
Normal file
43
.github/workflows/release.yml
vendored
Normal file
43
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Release to npm
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install
|
||||
uses: ./.github/actions/install
|
||||
with:
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
scope: '@thedaviddias'
|
||||
|
||||
- name: Setup npm auth
|
||||
run: |
|
||||
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
|
||||
echo "@thedaviddias:registry=https://registry.npmjs.org/" >> ~/.npmrc
|
||||
npm whoami || echo "Not logged in to npm"
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
echo "Attempting to publish with npm..."
|
||||
npm publish --access public || {
|
||||
echo "npm publish failed, trying with pnpm..."
|
||||
pnpm publish --no-git-checks --access public
|
||||
}
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NPM_CONFIG_REGISTRY: 'https://registry.npmjs.org'
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
build/
|
||||
*.log
|
||||
.env*
|
||||
94
README.md
Normal file
94
README.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# MCP LLMS.txt Explorer
|
||||
|
||||
A Model Context Protocol server for exploring websites with llms.txt files. This server helps you discover and analyze websites that implement the llms.txt standard.
|
||||
|
||||
## Features
|
||||
|
||||
### Resources
|
||||
- Check websites for llms.txt and llms-full.txt files
|
||||
- Parse and validate llms.txt file contents
|
||||
- Access structured data about compliant websites
|
||||
|
||||
### Tools
|
||||
- `check_website` - Check if a website has llms.txt files
|
||||
- Takes domain URL as input
|
||||
- Returns file locations and validation status
|
||||
- `list_websites` - List known websites with llms.txt files
|
||||
- Returns structured data about compliant websites
|
||||
- Supports filtering by file type (llms.txt/llms-full.txt)
|
||||
|
||||
## Development
|
||||
|
||||
Install dependencies:
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
Build the server:
|
||||
```bash
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
For development with auto-rebuild:
|
||||
```bash
|
||||
pnpm run watch
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
To use this server:
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/thedaviddias/mcp-llms-txt-explorer.git
|
||||
cd mcp-llms-txt-explorer
|
||||
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Build the server
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
### Configuration with Claude Desktop
|
||||
|
||||
To use with Claude Desktop, add the server config:
|
||||
|
||||
On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||
On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"llms-txt-explorer": {
|
||||
"command": "/path/to/llms-txt-explorer/build/index.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For npx usage, you can use:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"llms-txt-explorer": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@thedaviddias/mcp-llms-txt-explorer"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Debugging
|
||||
|
||||
Since MCP servers communicate over stdio, debugging can be challenging. We recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector), which is available as a package script:
|
||||
|
||||
```bash
|
||||
pnpm run inspector
|
||||
```
|
||||
|
||||
The Inspector will provide a URL to access debugging tools in your browser.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License—see the LICENSE file for details.
|
||||
45
package.json
Normal file
45
package.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@thedaviddias/mcp-llms-txt-explorer",
|
||||
"version": "0.1.0",
|
||||
"description": "A Model Context Protocol server for exploring websites with llms.txt files",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"mcp-llms-txt-explorer": "./build/index.js"
|
||||
},
|
||||
"files": [
|
||||
"build"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
|
||||
"prepare": "npm run build",
|
||||
"watch": "tsc --watch",
|
||||
"inspector": "npx @modelcontextprotocol/inspector build/index.js",
|
||||
"test": "tsc --noEmit",
|
||||
"prepublishOnly": "pnpm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "0.6.0",
|
||||
"@types/node-fetch": "^2.6.12",
|
||||
"node-fetch": "^3.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.24",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"keywords": [
|
||||
"mcp",
|
||||
"llms-txt",
|
||||
"model-context-protocol",
|
||||
"claude"
|
||||
],
|
||||
"packageManager": "pnpm@9.15.0",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"author": "David Dias",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/thedaviddias/mcp-llms-txt-explorer.git"
|
||||
}
|
||||
}
|
||||
357
pnpm-lock.yaml
generated
Normal file
357
pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,357 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@modelcontextprotocol/sdk':
|
||||
specifier: 0.6.0
|
||||
version: 0.6.0
|
||||
'@types/node-fetch':
|
||||
specifier: ^2.6.12
|
||||
version: 2.6.12
|
||||
node-fetch:
|
||||
specifier: ^3.3.2
|
||||
version: 3.3.2
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^20.11.24
|
||||
version: 20.17.23
|
||||
typescript:
|
||||
specifier: ^5.3.3
|
||||
version: 5.8.2
|
||||
|
||||
packages:
|
||||
|
||||
'@modelcontextprotocol/sdk@0.6.0':
|
||||
resolution: {integrity: sha512-9rsDudGhDtMbvxohPoMMyAUOmEzQsOK+XFchh6gZGqo8sx9sBuZQs+CUttXqa8RZXKDaJRCN2tUtgGof7jRkkw==}
|
||||
|
||||
'@types/node-fetch@2.6.12':
|
||||
resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==}
|
||||
|
||||
'@types/node@20.17.23':
|
||||
resolution: {integrity: sha512-8PCGZ1ZJbEZuYNTMqywO+Sj4vSKjSjT6Ua+6RFOYlEvIvKQABPtrNkoVSLSKDb4obYcMhspVKmsw8Cm10NFRUg==}
|
||||
|
||||
asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
|
||||
bytes@3.1.2:
|
||||
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
combined-stream@1.0.8:
|
||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
content-type@1.0.5:
|
||||
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
data-uri-to-buffer@4.0.1:
|
||||
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
delayed-stream@1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
depd@2.0.0:
|
||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-define-property@1.0.1:
|
||||
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-errors@1.3.0:
|
||||
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es-set-tostringtag@2.1.0:
|
||||
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
fetch-blob@3.2.0:
|
||||
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
|
||||
engines: {node: ^12.20 || >= 14.13}
|
||||
|
||||
form-data@4.0.2:
|
||||
resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
formdata-polyfill@4.0.10:
|
||||
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
|
||||
function-bind@1.1.2:
|
||||
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||
|
||||
get-intrinsic@1.3.0:
|
||||
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
get-proto@1.0.1:
|
||||
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
gopd@1.2.0:
|
||||
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
has-symbols@1.1.0:
|
||||
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
has-tostringtag@1.0.2:
|
||||
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hasown@2.0.2:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
http-errors@2.0.0:
|
||||
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
|
||||
math-intrinsics@1.1.0:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
mime-db@1.52.0:
|
||||
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
mime-types@2.1.35:
|
||||
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
node-domexception@1.0.0:
|
||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||
engines: {node: '>=10.5.0'}
|
||||
|
||||
node-fetch@3.3.2:
|
||||
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
raw-body@3.0.0:
|
||||
resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
safer-buffer@2.1.2:
|
||||
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
|
||||
|
||||
setprototypeof@1.2.0:
|
||||
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
|
||||
|
||||
statuses@2.0.1:
|
||||
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
toidentifier@1.0.1:
|
||||
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
typescript@5.8.2:
|
||||
resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
undici-types@6.19.8:
|
||||
resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==}
|
||||
|
||||
unpipe@1.0.0:
|
||||
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
web-streams-polyfill@3.3.3:
|
||||
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
zod@3.24.2:
|
||||
resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@modelcontextprotocol/sdk@0.6.0':
|
||||
dependencies:
|
||||
content-type: 1.0.5
|
||||
raw-body: 3.0.0
|
||||
zod: 3.24.2
|
||||
|
||||
'@types/node-fetch@2.6.12':
|
||||
dependencies:
|
||||
'@types/node': 20.17.23
|
||||
form-data: 4.0.2
|
||||
|
||||
'@types/node@20.17.23':
|
||||
dependencies:
|
||||
undici-types: 6.19.8
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
|
||||
bytes@3.1.2: {}
|
||||
|
||||
call-bind-apply-helpers@1.0.2:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
function-bind: 1.1.2
|
||||
|
||||
combined-stream@1.0.8:
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
|
||||
content-type@1.0.5: {}
|
||||
|
||||
data-uri-to-buffer@4.0.1: {}
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
depd@2.0.0: {}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
es-errors: 1.3.0
|
||||
gopd: 1.2.0
|
||||
|
||||
es-define-property@1.0.1: {}
|
||||
|
||||
es-errors@1.3.0: {}
|
||||
|
||||
es-object-atoms@1.1.1:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
|
||||
es-set-tostringtag@2.1.0:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
get-intrinsic: 1.3.0
|
||||
has-tostringtag: 1.0.2
|
||||
hasown: 2.0.2
|
||||
|
||||
fetch-blob@3.2.0:
|
||||
dependencies:
|
||||
node-domexception: 1.0.0
|
||||
web-streams-polyfill: 3.3.3
|
||||
|
||||
form-data@4.0.2:
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
es-set-tostringtag: 2.1.0
|
||||
mime-types: 2.1.35
|
||||
|
||||
formdata-polyfill@4.0.10:
|
||||
dependencies:
|
||||
fetch-blob: 3.2.0
|
||||
|
||||
function-bind@1.1.2: {}
|
||||
|
||||
get-intrinsic@1.3.0:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
es-define-property: 1.0.1
|
||||
es-errors: 1.3.0
|
||||
es-object-atoms: 1.1.1
|
||||
function-bind: 1.1.2
|
||||
get-proto: 1.0.1
|
||||
gopd: 1.2.0
|
||||
has-symbols: 1.1.0
|
||||
hasown: 2.0.2
|
||||
math-intrinsics: 1.1.0
|
||||
|
||||
get-proto@1.0.1:
|
||||
dependencies:
|
||||
dunder-proto: 1.0.1
|
||||
es-object-atoms: 1.1.1
|
||||
|
||||
gopd@1.2.0: {}
|
||||
|
||||
has-symbols@1.1.0: {}
|
||||
|
||||
has-tostringtag@1.0.2:
|
||||
dependencies:
|
||||
has-symbols: 1.1.0
|
||||
|
||||
hasown@2.0.2:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
http-errors@2.0.0:
|
||||
dependencies:
|
||||
depd: 2.0.0
|
||||
inherits: 2.0.4
|
||||
setprototypeof: 1.2.0
|
||||
statuses: 2.0.1
|
||||
toidentifier: 1.0.1
|
||||
|
||||
iconv-lite@0.6.3:
|
||||
dependencies:
|
||||
safer-buffer: 2.1.2
|
||||
|
||||
inherits@2.0.4: {}
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
mime-db@1.52.0: {}
|
||||
|
||||
mime-types@2.1.35:
|
||||
dependencies:
|
||||
mime-db: 1.52.0
|
||||
|
||||
node-domexception@1.0.0: {}
|
||||
|
||||
node-fetch@3.3.2:
|
||||
dependencies:
|
||||
data-uri-to-buffer: 4.0.1
|
||||
fetch-blob: 3.2.0
|
||||
formdata-polyfill: 4.0.10
|
||||
|
||||
raw-body@3.0.0:
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
http-errors: 2.0.0
|
||||
iconv-lite: 0.6.3
|
||||
unpipe: 1.0.0
|
||||
|
||||
safer-buffer@2.1.2: {}
|
||||
|
||||
setprototypeof@1.2.0: {}
|
||||
|
||||
statuses@2.0.1: {}
|
||||
|
||||
toidentifier@1.0.1: {}
|
||||
|
||||
typescript@5.8.2: {}
|
||||
|
||||
undici-types@6.19.8: {}
|
||||
|
||||
unpipe@1.0.0: {}
|
||||
|
||||
web-streams-polyfill@3.3.3: {}
|
||||
|
||||
zod@3.24.2: {}
|
||||
506
src/index.ts
Normal file
506
src/index.ts
Normal file
@@ -0,0 +1,506 @@
|
||||
#!/opt/homebrew/bin/node
|
||||
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListResourcesRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
ReadResourceRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import fetch from "node-fetch";
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { version } = require('../package.json');
|
||||
|
||||
const websites = 'https://raw.githubusercontent.com/thedaviddias/llms-txt-hub/main/data/websites.json'
|
||||
|
||||
/**
|
||||
* Type for a website with llms.txt information
|
||||
*/
|
||||
interface Website {
|
||||
name: string;
|
||||
domain: string;
|
||||
description: string;
|
||||
llmsTxtUrl?: string;
|
||||
llmsFullTxtUrl?: string;
|
||||
category?: string;
|
||||
favicon?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type for a linked content from llms.txt
|
||||
*/
|
||||
interface LinkedContent {
|
||||
url: string;
|
||||
content?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type for the check website result
|
||||
*/
|
||||
interface WebsiteCheckResult {
|
||||
hasLlmsTxt: boolean;
|
||||
hasLlmsFullTxt: boolean;
|
||||
llmsTxtUrl?: string;
|
||||
llmsFullTxtUrl?: string;
|
||||
llmsTxtContent?: string;
|
||||
llmsFullTxtContent?: string;
|
||||
linkedContents?: LinkedContent[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Known websites with llms.txt files
|
||||
* Initial data from llms-txt-hub
|
||||
*/
|
||||
let knownWebsites: Website[] = [];
|
||||
|
||||
/**
|
||||
* Cache for website check results
|
||||
*/
|
||||
const websiteCheckCache: { [domain: string]: WebsiteCheckResult } = {};
|
||||
|
||||
/**
|
||||
* Create an MCP server for exploring llms.txt files
|
||||
*/
|
||||
const server = new Server(
|
||||
{
|
||||
name: "LLMS.txt Explorer",
|
||||
version,
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
resources: {},
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Validate website data
|
||||
*/
|
||||
function isValidWebsite(website: unknown): website is Website {
|
||||
if (!website || typeof website !== 'object') return false;
|
||||
const w = website as Record<string, unknown>;
|
||||
return (
|
||||
typeof w.name === 'string' &&
|
||||
typeof w.domain === 'string' &&
|
||||
typeof w.description === 'string' &&
|
||||
(w.llmsTxtUrl === undefined || typeof w.llmsTxtUrl === 'string') &&
|
||||
(w.llmsFullTxtUrl === undefined || typeof w.llmsFullTxtUrl === 'string') &&
|
||||
(w.category === undefined || typeof w.category === 'string') &&
|
||||
(w.favicon === undefined || typeof w.favicon === 'string')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch websites list from GitHub
|
||||
*/
|
||||
async function fetchWebsitesList() {
|
||||
try {
|
||||
console.error('Fetching websites list from GitHub...');
|
||||
const response = await fetch(websites);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch websites list: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
throw new Error('Invalid data format: expected an array');
|
||||
}
|
||||
|
||||
const validWebsites = data.filter(isValidWebsite);
|
||||
console.error(`Fetched ${validWebsites.length} valid websites`);
|
||||
knownWebsites = validWebsites;
|
||||
} catch (error) {
|
||||
console.error('Error fetching websites list:', error);
|
||||
// Fallback to default website if fetch fails
|
||||
knownWebsites = [{
|
||||
name: "Supabase",
|
||||
domain: "https://supabase.com",
|
||||
description: "Build production-grade applications with Postgres",
|
||||
llmsTxtUrl: "https://supabase.com/llms.txt",
|
||||
category: "developer-tools"
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract linked URLs from llms.txt content
|
||||
*/
|
||||
function extractLinkedUrls(content: string): string[] {
|
||||
const urls: string[] = [];
|
||||
const lines = content.split('\n');
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim();
|
||||
if (trimmedLine.startsWith('@')) {
|
||||
const url = trimmedLine.slice(1).trim();
|
||||
if (url) {
|
||||
urls.push(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return urls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a website has llms.txt files
|
||||
*/
|
||||
async function checkWebsite(domain: string): Promise<WebsiteCheckResult> {
|
||||
console.error('Starting website check for:', domain);
|
||||
|
||||
// Return cached result if available
|
||||
if (websiteCheckCache[domain]) {
|
||||
console.error('Returning cached result for:', domain);
|
||||
return websiteCheckCache[domain];
|
||||
}
|
||||
|
||||
const result: WebsiteCheckResult = {
|
||||
hasLlmsTxt: false,
|
||||
hasLlmsFullTxt: false
|
||||
};
|
||||
|
||||
// Create an overall timeout for the entire operation
|
||||
const globalTimeout = new Promise<never>((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error('Global timeout exceeded'));
|
||||
}, 15000); // 15 second global timeout
|
||||
});
|
||||
|
||||
try {
|
||||
// Normalize domain and add protocol if missing
|
||||
let normalizedDomain = domain;
|
||||
if (!domain.startsWith('http://') && !domain.startsWith('https://')) {
|
||||
normalizedDomain = `https://${domain}`;
|
||||
}
|
||||
console.error('Normalized domain:', normalizedDomain);
|
||||
|
||||
// Validate URL format
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(normalizedDomain);
|
||||
} catch (e) {
|
||||
console.error('Invalid URL:', domain);
|
||||
throw new Error(`Invalid URL format: ${domain}`);
|
||||
}
|
||||
|
||||
// Use the normalized URL
|
||||
const baseUrl = url.origin;
|
||||
console.error('Base URL:', baseUrl);
|
||||
|
||||
// Helper function to fetch with timeout
|
||||
async function fetchWithTimeout(url: string, timeout = 5000) { // Reduced to 5 seconds
|
||||
console.error(`Fetching ${url} with ${timeout}ms timeout`);
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => {
|
||||
controller.abort();
|
||||
console.error(`Timeout after ${timeout}ms for ${url}`);
|
||||
}, timeout);
|
||||
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
const response = await fetch(url, {
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'User-Agent': 'llms-txt-explorer/0.1.0'
|
||||
}
|
||||
});
|
||||
const endTime = Date.now();
|
||||
console.error(`Fetch completed in ${endTime - startTime}ms for ${url}`);
|
||||
clearTimeout(timeoutId);
|
||||
return response;
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId);
|
||||
console.error(`Fetch error for ${url}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const checkPromise = (async () => {
|
||||
// Check for llms.txt
|
||||
try {
|
||||
const llmsTxtUrl = `${baseUrl}/llms.txt`;
|
||||
console.error('Fetching llms.txt from:', llmsTxtUrl);
|
||||
const llmsTxtRes = await fetchWithTimeout(llmsTxtUrl);
|
||||
console.error('llms.txt response status:', llmsTxtRes.status);
|
||||
|
||||
if (llmsTxtRes.ok) {
|
||||
result.hasLlmsTxt = true;
|
||||
result.llmsTxtUrl = llmsTxtUrl;
|
||||
const content = await llmsTxtRes.text();
|
||||
console.error(`llms.txt content length: ${content.length} bytes`);
|
||||
result.llmsTxtContent = content;
|
||||
console.error('Successfully fetched llms.txt');
|
||||
|
||||
// Extract and fetch linked contents in parallel with timeout
|
||||
const linkedUrls = extractLinkedUrls(content).slice(0, 3); // Reduced to 3 linked contents
|
||||
if (linkedUrls.length > 0) {
|
||||
console.error(`Found ${linkedUrls.length} linked URLs in llms.txt (limited to 3)`);
|
||||
result.linkedContents = [];
|
||||
|
||||
const fetchPromises = linkedUrls.map(async (url) => {
|
||||
console.error(`Fetching linked content from: ${url}`);
|
||||
try {
|
||||
const linkedRes = await fetchWithTimeout(url);
|
||||
if (!linkedRes.ok) {
|
||||
throw new Error(`Failed to fetch content: ${linkedRes.status}`);
|
||||
}
|
||||
const linkedContent = await linkedRes.text();
|
||||
console.error(`Linked content length: ${linkedContent.length} bytes`);
|
||||
return {
|
||||
url,
|
||||
content: linkedContent
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error fetching linked content from ${url}:`, error);
|
||||
return {
|
||||
url,
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for all fetches to complete with a 10 second timeout
|
||||
const linkedContentTimeout = new Promise<never>((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error('Linked content fetch timeout'));
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
try {
|
||||
result.linkedContents = await Promise.race([
|
||||
Promise.all(fetchPromises),
|
||||
linkedContentTimeout
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('Error fetching linked contents:', error);
|
||||
result.linkedContents = linkedUrls.map(url => ({
|
||||
url,
|
||||
error: 'Timeout fetching linked contents'
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.error('Error in main llms.txt fetch:', error);
|
||||
if (error instanceof Error) {
|
||||
result.error = error.message;
|
||||
} else {
|
||||
result.error = 'Unknown error fetching llms.txt';
|
||||
}
|
||||
}
|
||||
|
||||
// Only check llms-full.txt if llms.txt was successful
|
||||
if (result.hasLlmsTxt && !result.error) {
|
||||
try {
|
||||
const llmsFullTxtUrl = `${baseUrl}/llms-full.txt`;
|
||||
console.error('Fetching llms-full.txt from:', llmsFullTxtUrl);
|
||||
const llmsFullTxtRes = await fetchWithTimeout(llmsFullTxtUrl);
|
||||
console.error('llms-full.txt response status:', llmsFullTxtRes.status);
|
||||
|
||||
if (llmsFullTxtRes.ok) {
|
||||
result.hasLlmsFullTxt = true;
|
||||
result.llmsFullTxtUrl = llmsFullTxtUrl;
|
||||
const content = await llmsFullTxtRes.text();
|
||||
console.error(`llms-full.txt content length: ${content.length} bytes`);
|
||||
result.llmsFullTxtContent = content;
|
||||
console.error('Successfully fetched llms-full.txt');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching llms-full.txt:', error);
|
||||
// Don't fail the whole operation for llms-full.txt errors
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
})();
|
||||
|
||||
// Race between the check operation and the global timeout
|
||||
const finalResult = await Promise.race([checkPromise, globalTimeout]);
|
||||
|
||||
// Cache successful results only
|
||||
if (!finalResult.error) {
|
||||
websiteCheckCache[domain] = finalResult;
|
||||
}
|
||||
|
||||
console.error('Final result:', JSON.stringify(finalResult, null, 2));
|
||||
return finalResult;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
console.error('Error checking website:', errorMessage);
|
||||
return {
|
||||
hasLlmsTxt: false,
|
||||
hasLlmsFullTxt: false,
|
||||
error: errorMessage
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for listing available websites as resources
|
||||
*/
|
||||
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
||||
return {
|
||||
resources: knownWebsites.map(site => ({
|
||||
uri: `website://${site.domain}`,
|
||||
mimeType: "application/json",
|
||||
name: site.name,
|
||||
description: site.description
|
||||
}))
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Handler for reading website information
|
||||
*/
|
||||
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||
const url = new URL(request.params.uri);
|
||||
const domain = url.hostname;
|
||||
|
||||
const website = knownWebsites.find(site => new URL(site.domain).hostname === domain);
|
||||
if (!website) {
|
||||
throw new Error(`Website ${domain} not found in known websites`);
|
||||
}
|
||||
|
||||
const checkResult = await checkWebsite(website.domain);
|
||||
|
||||
return {
|
||||
contents: [{
|
||||
uri: request.params.uri,
|
||||
mimeType: "application/json",
|
||||
text: JSON.stringify({ ...website, ...checkResult }, null, 2)
|
||||
}]
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Handler that lists available tools
|
||||
*/
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: [
|
||||
{
|
||||
name: "check_website",
|
||||
description: "Check if a website has llms.txt files",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
url: {
|
||||
type: "string",
|
||||
description: "URL of the website to check"
|
||||
}
|
||||
},
|
||||
required: ["url"]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "list_websites",
|
||||
description: "List known websites with llms.txt files",
|
||||
inputSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
filter_llms_txt: {
|
||||
type: "boolean",
|
||||
description: "Only show websites with llms.txt"
|
||||
},
|
||||
filter_llms_full_txt: {
|
||||
type: "boolean",
|
||||
description: "Only show websites with llms-full.txt"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Handler for tool calls
|
||||
*/
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
console.error('Received tool request:', request.params.name);
|
||||
|
||||
switch (request.params.name) {
|
||||
case "check_website": {
|
||||
const url = String(request.params.arguments?.url);
|
||||
console.error('Checking website:', url);
|
||||
|
||||
if (!url) {
|
||||
console.error('URL is required');
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({ error: "URL is required" }, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await checkWebsite(url);
|
||||
console.error('Tool returning result:', JSON.stringify(result, null, 2));
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(result, null, 2)
|
||||
}]
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
console.error('Tool returning error:', errorMessage);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify({ error: errorMessage }, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
case "list_websites": {
|
||||
const filterLlmsTxt = Boolean(request.params.arguments?.filter_llms_txt);
|
||||
const filterLlmsFullTxt = Boolean(request.params.arguments?.filter_llms_full_txt);
|
||||
|
||||
let websites = knownWebsites;
|
||||
|
||||
if (filterLlmsTxt) {
|
||||
websites = websites.filter(site => site.llmsTxtUrl);
|
||||
}
|
||||
if (filterLlmsFullTxt) {
|
||||
websites = websites.filter(site => site.llmsFullTxtUrl);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(websites, null, 2)
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error("Unknown tool");
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Start the server using stdio transport
|
||||
*/
|
||||
async function main() {
|
||||
// Fetch websites list before starting the server
|
||||
await fetchWebsitesList();
|
||||
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Server error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
15
tsconfig.json
Normal file
15
tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"outDir": "./build",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user