Compare commits

...

52 Commits

Author SHA1 Message Date
e5e1c1e11c feat: enhance Obsidian API configuration with path support and implement async lifespan for server 2025-08-17 05:29:37 +00:00
cf48f23e8e fix: ensure trailing slashes on periodic endpoints to match Obsidian Local REST API paths 2025-08-16 19:08:14 +00:00
0b746f65e9 refactor: use centralized create_obsidian_api() for put_content to respect OBSIDIAN_HOST full URL format 2025-08-16 19:05:46 +00:00
897822ecaa fix: correctly parse legacy OBSIDIAN_HOST format 'host:port' to avoid double port in URL 2025-08-16 19:04:03 +00:00
00245ef40b fix: remove leftover merge markers in tools.py 2025-08-16 19:01:28 +00:00
8bca179ba5 fix: add full support for OBSIDIAN_HOST env var; URL parsing helpers; central API factory; README updates (merge of upstream PR #52) 2025-08-16 18:52:25 +00:00
Markus Pfundstein
72490a4db0 Merge pull request #44 from shipurjan/feature/add-put-content-tool
add obsidian_put_content tool
2025-06-28 11:04:04 +02:00
Markus Pfundstein
24cb2b9cbd Merge pull request #31 from sizhky/patch-1
Update README.md
2025-06-28 11:03:43 +02:00
Markus Pfundstein
741c554935 Merge pull request #58 from sfedyakov/complex-query-examples
Add JsonQuery examples to the description of Complex Query
2025-06-28 11:03:24 +02:00
Markus Pfundstein
0903b9bcc8 Merge pull request #59 from TheEpTic/main
Update obsidian.py
2025-06-28 11:03:03 +02:00
TheEpTic
63706f0968 Update obsidian.py
Add protocol and remove hardcoded localhost
2025-06-24 01:35:28 +01:00
Stanislav Fediakov
cefbd684bf Add JsonQuery examples to the description of Complex Query for it to work more reliably 2025-06-21 20:57:50 +04:00
Markus Pfundstein
8501758bfd Merge pull request #55 from vicampuzano/Metadata-for-periodic-notes
Adding a parameter to optionally retrieve metadata for periodic notes, in addition to just the content.
2025-06-19 11:36:35 +02:00
Markus Pfundstein
f1421fe292 Merge pull request #54 from TheConnMan/task/port-support
Add a port override env var
2025-06-19 11:36:00 +02:00
Victor Campuzano
168320f01b Adding a parameter to optionally retrieve metadata for periodic notes, in addition to just the content. 2025-06-12 15:57:07 +02:00
TheConnMan
64e28409cc Add a port override env var 2025-06-08 06:24:23 -04:00
Cyprian Zdebski
9df618eb3d Update descriptions 2025-04-17 22:58:24 +02:00
Cyprian Zdebski
7a8b723485 add obsidian_put_content tool 2025-04-17 22:51:56 +02:00
Markus Pfundstein
5cf106e30d Merge pull request #37 from felipemeres/feature/add-host-env-var
feat: add host environment variable support
2025-04-14 13:24:19 +02:00
Felipe Meres
44c8d1540f feat: add host environment variable support 2025-04-10 08:24:44 -04:00
sizhky
6de8d8cd28 Update README.md
Add a helpful statement below claude server config when claude sometimes fails to discover uvx in the system
2025-04-04 11:07:33 +05:30
Markus Pfundstein
51398322bf Merge pull request #27 from thomato/add-delete-function
Implement 'Delete file' functionality
2025-04-01 21:32:22 +02:00
Markus Pfundstein
f3a802da70 Merge pull request #30 from tibbon/patch-1
Add missing parens to README.md
2025-04-01 21:31:26 +02:00
David Fisher
0f55b50aaa Add missing parens to README.md
Example JSON is broken as-is. This fixes it.
2025-04-01 13:51:18 -04:00
Nando Thomassen
3f22521b01 Implement 'Delete file/directory' functionality
The MCP server now supports safe deletion of files and directories from
the Obsidian vault. A required confirmation parameter prevents accidental
deletions.
2025-03-29 22:29:01 +01:00
Markus Pfundstein
a86a6de1f4 Merge pull request #23 from jevy/period-tools
Update openapi.yaml and adding recent and periodic notes
2025-03-26 19:30:44 +01:00
Jevin Maltais
22177e44c9 Updating tools too 2025-03-22 08:53:59 -04:00
Jevin Maltais
182b42b567 Simplified queries 2025-03-22 08:50:04 -04:00
Jevin Maltais
dd7dfb56c4 Recent check works 2025-03-22 08:44:14 -04:00
Jevin Maltais
4c9cea7f30 Updating openapi and creating periodic tools 2025-03-22 07:59:36 -04:00
Markus Pfundstein
e6ef6a1e6d Merge pull request #16 from bathrobe/main
added get batch of file contents
2025-02-09 21:52:14 +01:00
joe
c813c12a06 added get batch of file contents 2025-02-01 12:15:03 -05:00
Markus Pfundstein
c1ae1eeec7 Merge pull request #13 from txbm/main
Update action names with obsidian_ prefix
2025-01-03 11:12:28 +01:00
Peter M. Elias
17a0e5b2b7 Update action names with obsidian_ prefix 2024-12-26 23:12:16 -08:00
Markus Pfundstein
07ced693ce Merge pull request #11 from punkpeye/patch-1
add MCP server badge
2024-12-19 13:04:46 +01:00
Markus Pfundstein
971c1edd34 Create LICENSE 2024-12-19 13:04:36 +01:00
Markus Pfundstein
0041778e4f Merge pull request #8 from Csaba8472/lower-python-version
lower python version
2024-12-19 13:03:08 +01:00
Frank Fiegel
0b65b77370 add MCP server badge
This PR adds a badge for the MCP server for Obsidian server listing in Glama MCP server directory.

<a href="https://glama.ai/mcp/servers/3wko1bhuek"><img width="380" height="200" src="https://glama.ai/mcp/servers/3wko1bhuek/badge" alt="server for Obsidian MCP server" /></a>

Glama performs regular codebase and documentation scans to:

* Confirm that the MCP server is working as expected
* Confirm that there are no obvious security issues with dependencies of the server
* Extract server characteristics such as tools, resources, prompts, and required parameters.

This badge helps your users to quickly asses that the MCP server is safe, server capabilities, and instructions for installing the server.
2024-12-17 09:26:20 -05:00
Csaba8472
2b7edd8283 lower python version 2024-12-14 21:53:19 +01:00
Markus Pfundstein
8494bdf83b more info 2024-12-05 10:15:10 +01:00
Markus Pfundstein
aa56549dc2 updated README 2024-12-05 10:11:51 +01:00
Markus Pfundstein
1b75b7db07 corrected server name. added cwd logging when .env is not found 2024-12-05 10:03:00 +01:00
Markus Pfundstein
c43fc39156 bumped mcp 2024-12-04 10:25:06 +01:00
Markus Pfundstein
abdc8fd875 renamed to mcp-obsidian 2024-12-04 10:22:24 +01:00
Markus Pfundstein
b88fc57b5d added pyright and fixed type errors 2024-12-03 21:36:54 +01:00
Markus Pfundstein
b1f68a4949 Merge pull request #4 from 7shi/add-timeout
Add timeout settings to HTTP requests
2024-12-01 20:01:55 +01:00
7shi
07e3697f27 Adjust timeout to align with MCP client timeouts 2024-12-02 02:59:21 +09:00
7shi
6d671ea0ff Add timeout settings to all requests in Obsidian Local REST API 2024-12-01 22:36:06 +09:00
Markus Pfundstein
970a9b061e Merge pull request #2 from 7shi/fix-quote
URL encode target header in PATCH requests
2024-12-01 10:50:13 +01:00
7shi
12ea3bc43c URL encode target header in PATCH requests 2024-12-01 05:15:47 +09:00
Markus Pfundstein
9e9547e45d Merge pull request #1 from eltociear/patch-1
chore: update openapi.yaml
2024-11-30 09:31:42 +01:00
Ikko Eltociear Ashimine
dd58a39d03 chore: update openapi.yaml
yoru -> your
2024-11-30 13:09:11 +09:00
12 changed files with 1796 additions and 539 deletions

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Markus Pfundstein
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.

View File

@@ -1,6 +1,8 @@
# mcp-knowledge-base MCP server
# MCP server for Obsidian
Example MCP server to interact with Obsidian.
MCP server to interact with Obsidian via the Local REST API community plugin.
<a href="https://glama.ai/mcp/servers/3wko1bhuek"><img width="380" height="200" src="https://glama.ai/mcp/servers/3wko1bhuek/badge" alt="server for Obsidian MCP server" /></a>
## Components
@@ -14,6 +16,7 @@ The server implements multiple tools to interact with Obsidian:
- search: Search for documents matching a specified text query across all files in the vault
- patch_content: Insert content into an existing note relative to a heading, block reference, or frontmatter field.
- append_content: Append content to a new or existing file in the vault.
- delete_file: Delete a file or directory from your vault.
### Example prompts
@@ -28,13 +31,54 @@ The use prompts like this:
### Environment Variables
Create a `.env` file in the root directory with the following required variable:
The MCP server requires the following environment variables:
- `OBSIDIAN_API_KEY`: Your Obsidian Local REST API key (required)
- `OBSIDIAN_HOST`: The URL for your Obsidian Local REST API (optional, defaults to `https://127.0.0.1:27124`)
#### OBSIDIAN_HOST Format
The `OBSIDIAN_HOST` variable accepts full URLs with protocol, host, and port. It supports both `localhost` and `127.0.0.1` with either `http` or `https`:
```
http://127.0.0.1:27123
https://localhost:27124
http://localhost:27123
https://127.0.0.1:27124
```
**Note:** The server performs a health check on startup. If the connection fails, you'll get an immediate error message indicating the configuration issue.
### Configuration Methods
There are two ways to configure the environment variables:
#### 1. Add to server config (preferred)
```json
{
"mcp-obsidian": {
"command": "uvx",
"args": [
"mcp-obsidian"
],
"env": {
"OBSIDIAN_API_KEY": "<your_api_key_here>",
"OBSIDIAN_HOST": "<your_obsidian_host>"
}
}
}
```
Sometimes Claude has issues detecting the location of uv / uvx. You can use `which uvx` to find and paste the full path in above config in such cases.
#### 2. Create a `.env` file in the working directory with the following required variable:
```
OBSIDIAN_API_KEY=your_api_key_here
OBSIDIAN_HOST=your_obsidian_host
```
Without this API key, the server will not be able to function.
**Note:** You can find the API key in the Obsidian Local REST API plugin configuration.
## Quickstart
@@ -58,14 +102,19 @@ On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
```json
{
"mcpServers": {
"mcp-knowledge-base": {
"mcp-obsidian": {
"command": "uv",
"args": [
"--directory",
"<dir_to>/mcp-knowledge-base",
"<dir_to>/mcp-obsidian",
"run",
"mcp-knowledge-base"
]
"mcp-obsidian"
],
"env": {
"OBSIDIAN_API_KEY": "<your_api_key_here>",
"OBSIDIAN_HOST": "<your_obsidian_host>",
"OBSIDIAN_PORT": "<your_obsidian_port>"
}
}
}
}
@@ -78,11 +127,15 @@ On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
```json
{
"mcpServers": {
"mcp-knowledge-base": {
"mcp-obsidian": {
"command": "uvx",
"args": [
"mcp-knowledge-base"
]
"mcp-obsidian"
],
"env": {
"OBSIDIAN_API_KEY": "<your_api_key_here>",
"OBSIDIAN_HOST": "<your_obsidian_host>"
}
}
}
}
@@ -91,7 +144,7 @@ On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
## Development
### Building and Publishing
### Building
To prepare the package for distribution:
@@ -100,22 +153,6 @@ To prepare the package for distribution:
uv sync
```
2. Build package distributions:
```bash
uv build
```
This will create source and wheel distributions in the `dist/` directory.
3. Publish to PyPI:
```bash
uv publish
```
Note: You'll need to set PyPI credentials via environment variables or command flags:
- Token: `--token` or `UV_PUBLISH_TOKEN`
- Or username/password: `--username`/`UV_PUBLISH_USERNAME` and `--password`/`UV_PUBLISH_PASSWORD`
### Debugging
Since MCP servers run over stdio, debugging can be challenging. For the best debugging
@@ -124,7 +161,7 @@ experience, we strongly recommend using the [MCP Inspector](https://github.com/m
You can launch the MCP Inspector via [`npm`](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) with this command:
```bash
npx @modelcontextprotocol/inspector uv --directory /path/to/mcp-knowledge-base run mcp-knowledge-base
npx @modelcontextprotocol/inspector uv --directory /path/to/mcp-obsidian run mcp-obsidian
```
Upon launching, the Inspector will display a URL that you can access in your browser to begin debugging.
@@ -132,5 +169,5 @@ Upon launching, the Inspector will display a URL that you can access in your bro
You can also watch the server logs with this command:
```bash
tail -n 20 -f ~/Library/Logs/Claude/mcp-server-mcp-knowledge-base.log
tail -n 20 -f ~/Library/Logs/Claude/mcp-server-mcp-obsidian.log
```

View File

@@ -149,7 +149,7 @@ paths:
- "Active File"
patch:
description: |
Inserts content into the currently-open note relative to a heading within that note.
Inserts content into the currently-open note relative to a heading, block refeerence, or frontmatter field within that document.
Allows you to modify the content relative to a heading, block reference, or frontmatter field in your document.
@@ -362,7 +362,7 @@ paths:
description: |
Your path references a directory instead of a file; this request method is valid only for updating files.
summary: |
Insert content into the currently open note in Obsidian relative to a heading within that document.
Insert content into the currently open note in Obsidian relative to a heading, block reference, or frontmatter field within that document.
tags:
- "Active File"
post:
@@ -403,6 +403,7 @@ paths:
tags:
- "Active File"
put:
parameters: []
requestBody:
content:
"*/*":
@@ -593,7 +594,7 @@ paths:
- "Periodic Notes"
patch:
description: |
Inserts content into an existing note relative to a heading within your note.
Inserts content into a periodic note relative to a heading, block refeerence, or frontmatter field within that document.
Allows you to modify the content relative to a heading, block reference, or frontmatter field in your document.
@@ -819,7 +820,7 @@ paths:
description: |
Your path references a directory instead of a file; this request method is valid only for updating files.
summary: |
Insert content into a periodic note relative to a heading within that document.
Insert content into a periodic note in Obsidian relative to a heading, block reference, or frontmatter field within that document.
tags:
- "Periodic Notes"
post:
@@ -920,6 +921,498 @@ paths:
Update the content of a periodic note.
tags:
- "Periodic Notes"
"/periodic/{year}/{month}/{day}/{period}/":
delete:
description: |
Deletes the periodic note for the specified period.
parameters:
- description: "The year of the date for which you would like to grab a periodic note."
in: "path"
name: "year"
required: true
schema:
type: "number"
- description: "The month (1-12) of the date for which you would like to grab a periodic note."
in: "path"
name: "month"
required: true
schema:
type: "number"
- description: "The day (1-31) of the date for which you would like to grab a periodic note."
in: "path"
name: "day"
required: true
schema:
type: "number"
- description: "The name of the period for which you would like to grab the current note."
in: "path"
name: "period"
required: true
schema:
default: "daily"
enum:
- "daily"
- "weekly"
- "monthly"
- "quarterly"
- "yearly"
type: "string"
responses:
"204":
description: "Success"
"404":
content:
application/json:
schema:
"$ref": "#/components/schemas/Error"
description: "File does not exist."
"405":
content:
application/json:
schema:
"$ref": "#/components/schemas/Error"
description: |
Your path references a directory instead of a file; this request method is valid only for updating files.
summary: |
Delete a periodic note.
tags:
- "Periodic Notes"
get:
parameters:
- description: "The year of the date for which you would like to grab a periodic note."
in: "path"
name: "year"
required: true
schema:
type: "number"
- description: "The month (1-12) of the date for which you would like to grab a periodic note."
in: "path"
name: "month"
required: true
schema:
type: "number"
- description: "The day (1-31) of the date for which you would like to grab a periodic note."
in: "path"
name: "day"
required: true
schema:
type: "number"
- description: "The name of the period for which you would like to grab the current note."
in: "path"
name: "period"
required: true
schema:
default: "daily"
enum:
- "daily"
- "weekly"
- "monthly"
- "quarterly"
- "yearly"
type: "string"
responses:
"200":
content:
"application/vnd.olrapi.note+json":
schema:
"$ref": "#/components/schemas/NoteJson"
text/markdown:
schema:
example: |
# This is my document
something else here
type: "string"
description: "Success"
"404":
description: "File does not exist"
summary: |
Get current periodic note for the specified period.
tags:
- "Periodic Notes"
patch:
description: |
Inserts content into a periodic note relative to a heading, block refeerence, or frontmatter field within that document.
Allows you to modify the content relative to a heading, block reference, or frontmatter field in your document.
Note that this API was changed in Version 3.0 of this extension and the earlier PATCH API is now deprecated. Requests made using the previous version of this API will continue to work until Version 4.0 is released. See https://github.com/coddingtonbear/obsidian-local-rest-api/wiki/Changes-to-PATCH-requests-between-versions-2.0-and-3.0 for more details and migration instructions.
# Examples
All of the below examples assume you have a document that looks like
this:
```markdown
---
alpha: 1
beta: test
delta:
zeta: 1
yotta: 1
gamma:
- one
- two
---
# Heading 1
This is the content for heading one
Also references some [[#^484ef2]]
## Subheading 1:1
Content for Subheading 1:1
### Subsubheading 1:1:1
### Subsubheading 1:1:2
Testing how block references work for a table.[[#^2c7cfa]]
Some content for Subsubheading 1:1:2
More random text.
^2d9b4a
## Subheading 1:2
Content for Subheading 1:2.
some content with a block reference ^484ef2
## Subheading 1:3
| City | Population |
| ------------ | ---------- |
| Seattle, WA | 8 |
| Portland, OR | 4 |
^2c7cfa
```
## Append Content Below a Heading
If you wanted to append the content "Hello" below "Subheading 1:1:1" under "Heading 1",
you could send a request with the following headers:
- `Operation`: `append`
- `Target-Type`: `heading`
- `Target`: `Heading 1::Subheading 1:1:1`
- with the request body: `Hello`
The above would work just fine for `prepend` or `replace`, too, of course,
but with different results.
## Append Content to a Block Reference
If you wanted to append the content "Hello" below the block referenced by
"2d9b4a" above ("More random text."), you could send the following headers:
- `Operation`: `append`
- `Target-Type`: `block`
- `Target`: `2d9b4a`
- with the request body: `Hello`
The above would work just fine for `prepend` or `replace`, too, of course,
but with different results.
## Add a Row to a Table Referenced by a Block Reference
If you wanted to add a new city ("Chicago, IL") and population ("16") pair to the table above
referenced by the block reference `2c7cfa`, you could send the following
headers:
- `Operation`: `append`
- `TargetType`: `block`
- `Target`: `2c7cfa`
- `Content-Type`: `application/json`
- with the request body: `[["Chicago, IL", "16"]]`
The use of a `Content-Type` of `application/json` allows the API
to infer that member of your array represents rows and columns of your
to append to the referenced table. You can of course just use a
`Content-Type` of `text/markdown`, but in such a case you'll have to
format your table row manually instead of letting the library figure
it out for you.
You also have the option of using `prepend` (in which case, your new
row would be the first -- right below the table heading) or `replace` (in which
case all rows except the table heading would be replaced by the new row(s)
you supplied).
## Setting a Frontmatter Field
If you wanted to set the frontmatter field `alpha` to `2`, you could
send the following headers:
- `Operation`: `replace`
- `TargetType`: `frontmatter`
- `Target`: `beep`
- with the request body `2`
If you're setting a frontmatter field that might not already exist
you may want to use the `Create-Target-If-Missing` header so the
new frontmatter field is created and set to your specified value
if it doesn't already exist.
You may find using a `Content-Type` of `application/json` to be
particularly useful in the case of frontmatter since frontmatter
fields' values are JSON data, and the API can be smarter about
interpreting yoru `prepend` or `append` requests if you specify
your data as JSON (particularly when appending, for example,
list items).
parameters:
- description: "Patch operation to perform"
in: "header"
name: "Operation"
required: true
schema:
enum:
- "append"
- "prepend"
- "replace"
type: "string"
- description: "Type of target to patch"
in: "header"
name: "Target-Type"
required: true
schema:
enum:
- "heading"
- "block"
- "frontmatter"
type: "string"
- description: "Delimiter to use for nested targets (i.e. Headings)"
in: "header"
name: "Target-Delimiter"
required: false
schema:
default: "::"
type: "string"
- description: |
Target to patch; this value can be URL-Encoded and *must*
be URL-Encoded if it includes non-ASCII characters.
in: "header"
name: "Target"
required: true
schema:
type: "string"
- description: "Trim whitespace from Target before applying patch?"
in: "header"
name: "Trim-Target-Whitespace"
required: false
schema:
default: "false"
enum:
- "true"
- "false"
type: "string"
- description: "The year of the date for which you would like to grab a periodic note."
in: "path"
name: "year"
required: true
schema:
type: "number"
- description: "The month (1-12) of the date for which you would like to grab a periodic note."
in: "path"
name: "month"
required: true
schema:
type: "number"
- description: "The day (1-31) of the date for which you would like to grab a periodic note."
in: "path"
name: "day"
required: true
schema:
type: "number"
- description: "The name of the period for which you would like to grab the current note."
in: "path"
name: "period"
required: true
schema:
default: "daily"
enum:
- "daily"
- "weekly"
- "monthly"
- "quarterly"
- "yearly"
type: "string"
requestBody:
content:
application/json:
schema:
example: "['one', 'two']"
type: "string"
text/markdown:
schema:
example: |
# This is my document
something else here
type: "string"
description: "Content you would like to insert."
required: true
responses:
"200":
description: "Success"
"400":
content:
application/json:
schema:
"$ref": "#/components/schemas/Error"
description: "Bad Request; see response message for details."
"404":
content:
application/json:
schema:
"$ref": "#/components/schemas/Error"
description: "Does not exist"
"405":
content:
application/json:
schema:
"$ref": "#/components/schemas/Error"
description: |
Your path references a directory instead of a file; this request method is valid only for updating files.
summary: |
Insert content into a periodic note in Obsidian relative to a heading, block reference, or frontmatter field within that document.
tags:
- "Periodic Notes"
post:
description: |
Appends content to the periodic note for the specified period. This will create the relevant periodic note if necessary.
parameters:
- description: "The year of the date for which you would like to grab a periodic note."
in: "path"
name: "year"
required: true
schema:
type: "number"
- description: "The month (1-12) of the date for which you would like to grab a periodic note."
in: "path"
name: "month"
required: true
schema:
type: "number"
- description: "The day (1-31) of the date for which you would like to grab a periodic note."
in: "path"
name: "day"
required: true
schema:
type: "number"
- description: "The name of the period for which you would like to grab the current note."
in: "path"
name: "period"
required: true
schema:
default: "daily"
enum:
- "daily"
- "weekly"
- "monthly"
- "quarterly"
- "yearly"
type: "string"
requestBody:
content:
text/markdown:
schema:
example: |
# This is my document
something else here
type: "string"
description: "Content you would like to append."
required: true
responses:
"204":
description: "Success"
"400":
content:
application/json:
schema:
"$ref": "#/components/schemas/Error"
description: "Bad Request"
"405":
content:
application/json:
schema:
"$ref": "#/components/schemas/Error"
description: |
Your path references a directory instead of a file; this request method is valid only for updating files.
summary: |
Append content to a periodic note.
tags:
- "Periodic Notes"
put:
parameters:
- description: "The year of the date for which you would like to grab a periodic note."
in: "path"
name: "year"
required: true
schema:
type: "number"
- description: "The month (1-12) of the date for which you would like to grab a periodic note."
in: "path"
name: "month"
required: true
schema:
type: "number"
- description: "The day (1-31) of the date for which you would like to grab a periodic note."
in: "path"
name: "day"
required: true
schema:
type: "number"
- description: "The name of the period for which you would like to grab the current note."
in: "path"
name: "period"
required: true
schema:
default: "daily"
enum:
- "daily"
- "weekly"
- "monthly"
- "quarterly"
- "yearly"
type: "string"
requestBody:
content:
"*/*":
schema:
type: "string"
text/markdown:
schema:
example: |
# This is my document
something else here
type: "string"
description: "Content of the file you would like to upload."
required: true
responses:
"204":
description: "Success"
"400":
content:
application/json:
schema:
"$ref": "#/components/schemas/Error"
description: |
Incoming file could not be processed. Make sure you have specified a reasonable file name, and make sure you have set a reasonable 'Content-Type' header; if you are uploading a note, 'text/markdown' is likely the right choice.
"405":
content:
application/json:
schema:
"$ref": "#/components/schemas/Error"
description: |
Your path references a directory instead of a file; this request method is valid only for updating files.
summary: |
Update the content of a periodic note.
tags:
- "Periodic Notes"
/search/:
post:
description: |
@@ -1195,7 +1688,7 @@ paths:
- "Vault Files"
patch:
description: |
Inserts content into an existing note relative to a heading within your note.
Inserts content into an existing note relative to a heading, block refeerence, or frontmatter field within that document.
Allows you to modify the content relative to a heading, block reference, or frontmatter field in your document.
@@ -1416,14 +1909,14 @@ paths:
description: |
Your path references a directory instead of a file; this request method is valid only for updating files.
summary: |
Insert content into an existing note relative to a heading within that document.
Insert content into an existing note in Obsidian relative to a heading, block reference, or frontmatter field within that document.
tags:
- "Vault Files"
post:
description: |
Appends content to the end of an existing note. If the specified file does not yet exist, it will be created as an empty file.
If you would like to insert text relative to a particular heading instead of appending to the end of the file, see 'patch'.
If you would like to insert text relative to a particular heading, block reference, or frontmatter field instead of appending to the end of the file, see 'patch'.
parameters:
- description: |
Path to the relevant file (relative to your vault root).

View File

@@ -1,11 +1,11 @@
[project]
name = "mcp-knowledge-base"
version = "0.1.0"
description = "Example MCP server to create a knowledge-base"
name = "mcp-obsidian"
version = "0.2.1"
description = "MCP server to work with Obsidian via the remote REST plugin"
readme = "README.md"
requires-python = ">=3.13"
requires-python = ">=3.11"
dependencies = [
"mcp>=1.0.0",
"mcp>=1.1.0",
"python-dotenv>=1.0.1",
"requests>=2.32.3",
]
@@ -17,5 +17,10 @@ email = "markus@life-electronic.nl"
requires = [ "hatchling",]
build-backend = "hatchling.build"
[dependency-groups]
dev = [
"pyright>=1.1.389",
]
[project.scripts]
mcp-knowledge-base = "mcp_knowledge_base:main"
mcp-obsidian = "mcp_obsidian:main"

View File

@@ -1,131 +0,0 @@
import requests
from typing import Any
class Obsidian():
def __init__(
self,
api_key: str,
protocol: str = 'https',
host: str = "127.0.0.1",
port: int = 27124,
verify_ssl: bool = False,
):
self.api_key = api_key
self.protocol = protocol
self.host = host
self.port = port
self.verify_ssl = verify_ssl
def get_base_url(self) -> str:
return f'{self.protocol}://{self.host}:{self.port}'
def _get_headers(self) -> dict:
headers = {
'Authorization': f'Bearer {self.api_key}'
}
return headers
def _safe_call(self, f) -> Any:
try:
return f()
except requests.HTTPError as e:
error_data = e.response.json() if e.response.content else {}
code = error_data.get('errorCode', -1)
message = error_data.get('message', '<unknown>')
raise Exception(f"Error {code}: {message}")
except requests.exceptions.RequestException as e:
raise Exception(f"Request failed: {str(e)}")
def list_files_in_vault(self) -> Any:
url = f"{self.get_base_url()}/vault/"
def call_fn():
response = requests.get(url, headers=self._get_headers(), verify=self.verify_ssl)
response.raise_for_status()
return response.json()['files']
return self._safe_call(call_fn)
def list_files_in_dir(self, dirpath: str) -> Any:
url = f"{self.get_base_url()}/vault/{dirpath}/"
def call_fn():
response = requests.get(url, headers=self._get_headers(), verify=self.verify_ssl)
response.raise_for_status()
return response.json()['files']
return self._safe_call(call_fn)
def get_file_contents(self, filepath: str) -> Any:
url = f"{self.get_base_url()}/vault/{filepath}"
def call_fn():
response = requests.get(url, headers=self._get_headers(), verify=self.verify_ssl)
response.raise_for_status()
return response.text
return self._safe_call(call_fn)
def search(self, query: str, context_length: int = 100) -> Any:
url = f"{self.get_base_url()}/search/simple/"
params = {
'query': query,
'contextLength': context_length
}
def call_fn():
response = requests.post(url, headers=self._get_headers(), params=params, verify=self.verify_ssl)
response.raise_for_status()
return response.json()
return self._safe_call(call_fn)
def append_content(self, filepath: str, content: str) -> Any:
url = f"{self.get_base_url()}/vault/{filepath}"
def call_fn():
response = requests.post(
url,
headers=self._get_headers() | {'Content-Type': 'text/markdown'},
data=content,
verify=self.verify_ssl
)
response.raise_for_status()
return None
return self._safe_call(call_fn)
def patch_content(self, filepath: str, operation: str, target_type: str, target: str, content: str) -> Any:
url = f"{self.get_base_url()}/vault/{filepath}"
headers = self._get_headers() | {
'Content-Type': 'text/markdown',
'Operation': operation,
'Target-Type': target_type,
'Target': target
}
def call_fn():
response = requests.patch(url, headers=headers, data=content, verify=self.verify_ssl)
response.raise_for_status()
return None
return self._safe_call(call_fn)
def search_json(self, query: dict) -> Any:
url = f"{self.get_base_url()}/search/"
headers = self._get_headers() | {
'Content-Type': 'application/vnd.olrapi.jsonlogic+json'
}
def call_fn():
response = requests.post(url, headers=headers, json=query, verify=self.verify_ssl)
response.raise_for_status()
return response.json()
return self._safe_call(call_fn)

View File

@@ -1,327 +0,0 @@
from collections.abc import Sequence
from mcp.types import (
Tool,
TextContent,
ImageContent,
EmbeddedResource,
LoggingLevel,
)
import json
import os
from . import obsidian
api_key = os.getenv("OBSIDIAN_API_KEY")
if not api_key:
raise ValueError("OBSIDIAN_API_KEY environment variable required")
TOOL_LIST_FILES_IN_VAULT = "list_files_in_vault"
TOOL_LIST_FILES_IN_DIR = "list_files_in_dir"
class ToolHandler():
def __init__(self, tool_name: str):
self.name = tool_name
def get_tool_description(self) -> Tool:
raise NotImplementedError()
def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
raise NotImplementedError()
class ListFilesInVaultToolHandler(ToolHandler):
def __init__(self):
super().__init__(TOOL_LIST_FILES_IN_VAULT)
def get_tool_description(self):
return Tool(
name=self.name,
description="Lists all files and directories in the root directory of your Obsidian vault.",
inputSchema={
"type": "object",
"properties": {},
"required": []
},
)
def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
api = obsidian.Obsidian(api_key=api_key)
files = api.list_files_in_vault()
return [
TextContent(
type="text",
text=json.dumps(files, indent=2)
)
]
class ListFilesInDirToolHandler(ToolHandler):
def __init__(self):
super().__init__(TOOL_LIST_FILES_IN_DIR)
def get_tool_description(self):
return Tool(
name=self.name,
description="Lists all files and directories that exist in a specific Obsidian directory.",
inputSchema={
"type": "object",
"properties": {
"dirpath": {
"type": "string",
"description": "Path to list files from (relative to your vault root). Note that empty directories will not be returned."
},
},
"required": ["dirpath"]
}
)
def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
if "dirpath" not in args:
raise RuntimeError("dirpath argument missing in arguments")
api = obsidian.Obsidian(api_key=api_key)
files = api.list_files_in_dir(args["dirpath"])
return [
TextContent(
type="text",
text=json.dumps(files, indent=2)
)
]
class GetFileContentsToolHandler(ToolHandler):
def __init__(self):
super().__init__("get_file_contents")
def get_tool_description(self):
return Tool(
name=self.name,
description="Return the content of a single file in your vault.",
inputSchema={
"type": "object",
"properties": {
"filepath": {
"type": "string",
"description": "Path to the relevant file (relative to your vault root).",
"format": "path"
},
},
"required": ["filepath"]
}
)
def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
if "filepath" not in args:
raise RuntimeError("filepath argument missing in arguments")
api = obsidian.Obsidian(api_key=api_key)
content = api.get_file_contents(args["filepath"])
return [
TextContent(
type="text",
text=json.dumps(content, indent=2)
)
]
class SearchToolHandler(ToolHandler):
def __init__(self):
super().__init__("simple_search")
def get_tool_description(self):
return Tool(
name=self.name,
description="""Simple search for documents matching a specified text query across all files in the vault.
Use this tool when you want to do a simple text search""",
inputSchema={
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Text to a simple search for in the vault."
},
"context_length": {
"type": "integer",
"description": "How much context to return around the matching string (default: 100)",
"default": 100
}
},
"required": ["query"]
}
)
def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
if "query" not in args:
raise RuntimeError("query argument missing in arguments")
context_length = args.get("context_length", 100)
api = obsidian.Obsidian(api_key=api_key)
results = api.search(args["query"], context_length)
formatted_results = []
for result in results:
formatted_matches = []
for match in result.get('matches', []):
context = match.get('context', '')
match_pos = match.get('match', {})
start = match_pos.get('start', 0)
end = match_pos.get('end', 0)
formatted_matches.append({
'context': context,
'match_position': {'start': start, 'end': end}
})
formatted_results.append({
'filename': result.get('filename', ''),
'score': result.get('score', 0),
'matches': formatted_matches
})
return [
TextContent(
type="text",
text=json.dumps(formatted_results, indent=2)
)
]
class AppendContentToolHandler(ToolHandler):
def __init__(self):
super().__init__("append_content")
def get_tool_description(self):
return Tool(
name=self.name,
description="Append content to a new or existing file in the vault.",
inputSchema={
"type": "object",
"properties": {
"filepath": {
"type": "string",
"description": "Path to the file (relative to vault root)",
"format": "path"
},
"content": {
"type": "string",
"description": "Content to append to the file"
}
},
"required": ["filepath", "content"]
}
)
def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
if "filepath" not in args or "content" not in args:
raise RuntimeError("filepath and content arguments required")
api = obsidian.Obsidian(api_key=api_key)
api.append_content(args["filepath"], args["content"])
return [
TextContent(
type="text",
text=f"Successfully appended content to {args['filepath']}"
)
]
class PatchContentToolHandler(ToolHandler):
def __init__(self):
super().__init__("patch_content")
def get_tool_description(self):
return Tool(
name=self.name,
description="Insert content into an existing note relative to a heading, block reference, or frontmatter field.",
inputSchema={
"type": "object",
"properties": {
"filepath": {
"type": "string",
"description": "Path to the file (relative to vault root)",
"format": "path"
},
"operation": {
"type": "string",
"description": "Operation to perform (append, prepend, or replace)",
"enum": ["append", "prepend", "replace"]
},
"target_type": {
"type": "string",
"description": "Type of target to patch",
"enum": ["heading", "block", "frontmatter"]
},
"target": {
"type": "string",
"description": "Target identifier (heading path, block reference, or frontmatter field)"
},
"content": {
"type": "string",
"description": "Content to insert"
}
},
"required": ["filepath", "operation", "target_type", "target", "content"]
}
)
def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
required = ["filepath", "operation", "target_type", "target", "content"]
if not all(key in args for key in required):
raise RuntimeError(f"Missing required arguments: {', '.join(required)}")
api = obsidian.Obsidian(api_key=api_key)
api.patch_content(
args["filepath"],
args["operation"],
args["target_type"],
args["target"],
args["content"]
)
return [
TextContent(
type="text",
text=f"Successfully patched content in {args['filepath']}"
)
]
class ComplexSearchToolHandler(ToolHandler):
def __init__(self):
super().__init__("complex_search")
def get_tool_description(self):
return Tool(
name=self.name,
description="""Complex search for documents using a JsonLogic query.
Supports standard JsonLogic operators plus 'glob' and 'regexp' for pattern matching. Results must be non-falsy.
Use this tool when you want to do a complex search, e.g. for all documents with certain tags etc.
""",
inputSchema={
"type": "object",
"properties": {
"query": {
"type": "object",
"description": "JsonLogic query object. Example: {\"glob\": [\"*.md\", {\"var\": \"path\"}]} matches all markdown files"
}
},
"required": ["query"]
}
)
def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
if "query" not in args:
raise RuntimeError("query argument missing in arguments")
api = obsidian.Obsidian(api_key=api_key)
results = api.search_json(args["query"])
return [
TextContent(
type="text",
text=json.dumps(results, indent=2)
)
]

View File

@@ -0,0 +1,374 @@
import requests
import urllib.parse
import os
from typing import Any, Tuple
from urllib.parse import urlparse
class Obsidian():
def __init__(
self,
api_key: str,
protocol: str = os.getenv('OBSIDIAN_PROTOCOL', 'https').lower(),
host: str = str(os.getenv('OBSIDIAN_HOST', '127.0.0.1')),
port: int = int(os.getenv('OBSIDIAN_PORT', '27124')),
path: str = '',
verify_ssl: bool = False,
):
self.api_key = api_key
if protocol == 'http':
self.protocol = 'http'
else:
self.protocol = 'https' # Default to https for any other value, including 'https'
self.host = host
self.port = port
self.path = path.rstrip('/')
self.verify_ssl = verify_ssl
self.timeout = (3, 6)
@classmethod
def from_url(cls, api_key: str, url: str, verify_ssl: bool = False) -> 'Obsidian':
"""Create Obsidian instance from a full URL.
Args:
api_key: The API key for authentication
url: Full URL like 'http://127.0.0.1:27123' or 'https://localhost:27124'
verify_ssl: Whether to verify SSL certificates
Returns:
Obsidian instance with parsed URL components
Raises:
ValueError: If URL is malformed or missing required components
"""
try:
parsed = urlparse(url)
if not parsed.scheme:
raise ValueError(f"URL must include protocol (http/https): {url}")
if not parsed.hostname:
raise ValueError(f"URL must include hostname: {url}")
protocol = parsed.scheme
host = parsed.hostname
port = parsed.port
path = parsed.path
# Set default ports based on protocol if not specified
if port is None:
port = 27124 if protocol == 'https' else 27123
return cls(
api_key=api_key,
protocol=protocol,
host=host,
port=port,
path=path,
verify_ssl=verify_ssl
)
except Exception as e:
raise ValueError(f"Failed to parse OBSIDIAN_HOST URL '{url}': {str(e)}")
@staticmethod
def parse_host_config(host_config: str) -> Tuple[str, str, int, str]:
"""Parse host configuration string.
Args:
host_config: Either a full URL (http://host:port) or just hostname/IP
Returns:
Tuple of (protocol, host, port, path)
"""
if '://' in host_config:
# Full URL format
parsed = urlparse(host_config)
protocol = parsed.scheme or 'https'
host = parsed.hostname or '127.0.0.1'
port = parsed.port or (27124 if protocol == 'https' else 27123)
path = parsed.path
else:
# Support legacy formats
# 1) hostname/IP only
# 2) hostname:port (no protocol)
if ':' in host_config:
# Treat as host:port and default protocol to https
parsed = urlparse(f'https://{host_config}')
protocol = 'https'
host = parsed.hostname or '127.0.0.1'
port = parsed.port or 27124
path = ''
else:
protocol = 'https'
host = host_config
port = 27124
path = ''
return protocol, host, port, path
def get_base_url(self) -> str:
return f'{self.protocol}://{self.host}:{self.port}{self.path}'
def _get_headers(self) -> dict:
headers = {
'Authorization': f'Bearer {self.api_key}'
}
return headers
def _safe_call(self, f) -> Any:
try:
return f()
except requests.HTTPError as e:
error_data = e.response.json() if e.response.content else {}
code = error_data.get('errorCode', -1)
message = error_data.get('message', '<unknown>')
raise Exception(f"Error {code}: {message}")
except requests.exceptions.RequestException as e:
raise Exception(f"Request failed: {str(e)}")
def list_files_in_vault(self) -> Any:
url = f"{self.get_base_url()}/vault/"
def call_fn():
response = requests.get(url, headers=self._get_headers(), verify=self.verify_ssl, timeout=self.timeout)
response.raise_for_status()
return response.json()['files']
return self._safe_call(call_fn)
def list_files_in_dir(self, dirpath: str) -> Any:
url = f"{self.get_base_url()}/vault/{dirpath}/"
def call_fn():
response = requests.get(url, headers=self._get_headers(), verify=self.verify_ssl, timeout=self.timeout)
response.raise_for_status()
return response.json()['files']
return self._safe_call(call_fn)
def get_file_contents(self, filepath: str) -> Any:
url = f"{self.get_base_url()}/vault/{filepath}"
def call_fn():
response = requests.get(url, headers=self._get_headers(), verify=self.verify_ssl, timeout=self.timeout)
response.raise_for_status()
return response.text
return self._safe_call(call_fn)
def get_batch_file_contents(self, filepaths: list[str]) -> str:
"""Get contents of multiple files and concatenate them with headers.
Args:
filepaths: List of file paths to read
Returns:
String containing all file contents with headers
"""
result = []
for filepath in filepaths:
try:
content = self.get_file_contents(filepath)
result.append(f"# {filepath}\n\n{content}\n\n---\n\n")
except Exception as e:
# Add error message but continue processing other files
result.append(f"# {filepath}\n\nError reading file: {str(e)}\n\n---\n\n")
return "".join(result)
def search(self, query: str, context_length: int = 100) -> Any:
url = f"{self.get_base_url()}/search/simple/"
params = {
'query': query,
'contextLength': context_length
}
def call_fn():
response = requests.post(url, headers=self._get_headers(), params=params, verify=self.verify_ssl, timeout=self.timeout)
response.raise_for_status()
return response.json()
return self._safe_call(call_fn)
def append_content(self, filepath: str, content: str) -> Any:
url = f"{self.get_base_url()}/vault/{filepath}"
def call_fn():
response = requests.post(
url,
headers=self._get_headers() | {'Content-Type': 'text/markdown'},
data=content,
verify=self.verify_ssl,
timeout=self.timeout
)
response.raise_for_status()
return None
return self._safe_call(call_fn)
def patch_content(self, filepath: str, operation: str, target_type: str, target: str, content: str) -> Any:
url = f"{self.get_base_url()}/vault/{filepath}"
headers = self._get_headers() | {
'Content-Type': 'text/markdown',
'Operation': operation,
'Target-Type': target_type,
'Target': urllib.parse.quote(target)
}
def call_fn():
response = requests.patch(url, headers=headers, data=content, verify=self.verify_ssl, timeout=self.timeout)
response.raise_for_status()
return None
return self._safe_call(call_fn)
def put_content(self, filepath: str, content: str) -> Any:
url = f"{self.get_base_url()}/vault/{filepath}"
def call_fn():
response = requests.put(
url,
headers=self._get_headers() | {'Content-Type': 'text/markdown'},
data=content,
verify=self.verify_ssl,
timeout=self.timeout
)
response.raise_for_status()
return None
return self._safe_call(call_fn)
def delete_file(self, filepath: str) -> Any:
"""Delete a file or directory from the vault.
Args:
filepath: Path to the file to delete (relative to vault root)
Returns:
None on success
"""
url = f"{self.get_base_url()}/vault/{filepath}"
def call_fn():
response = requests.delete(url, headers=self._get_headers(), verify=self.verify_ssl, timeout=self.timeout)
response.raise_for_status()
return None
return self._safe_call(call_fn)
def search_json(self, query: dict) -> Any:
url = f"{self.get_base_url()}/search/"
headers = self._get_headers() | {
'Content-Type': 'application/vnd.olrapi.jsonlogic+json'
}
def call_fn():
response = requests.post(url, headers=headers, json=query, verify=self.verify_ssl, timeout=self.timeout)
response.raise_for_status()
return response.json()
return self._safe_call(call_fn)
def get_periodic_note(self, period: str, type: str = "content") -> Any:
"""Get current periodic note for the specified period.
Args:
period: The period type (daily, weekly, monthly, quarterly, yearly)
type: Type of the data to get ('content' or 'metadata').
'content' returns just the content in Markdown format.
'metadata' includes note metadata (including paths, tags, etc.) and the content..
Returns:
Content of the periodic note
"""
url = f"{self.get_base_url()}/periodic/{period}/"
def call_fn():
headers = self._get_headers()
if type == "metadata":
headers['Accept'] = 'application/vnd.olrapi.note+json'
response = requests.get(url, headers=headers, verify=self.verify_ssl, timeout=self.timeout)
response.raise_for_status()
return response.text
return self._safe_call(call_fn)
def get_recent_periodic_notes(self, period: str, limit: int = 5, include_content: bool = False) -> Any:
"""Get most recent periodic notes for the specified period type.
Args:
period: The period type (daily, weekly, monthly, quarterly, yearly)
limit: Maximum number of notes to return (default: 5)
include_content: Whether to include note content (default: False)
Returns:
List of recent periodic notes
"""
url = f"{self.get_base_url()}/periodic/{period}/recent/"
params = {
"limit": limit,
"includeContent": include_content
}
def call_fn():
response = requests.get(
url,
headers=self._get_headers(),
params=params,
verify=self.verify_ssl,
timeout=self.timeout
)
response.raise_for_status()
return response.json()
return self._safe_call(call_fn)
def get_recent_changes(self, limit: int = 10, days: int = 90) -> Any:
"""Get recently modified files in the vault.
Args:
limit: Maximum number of files to return (default: 10)
days: Only include files modified within this many days (default: 90)
Returns:
List of recently modified files with metadata
"""
# Build the DQL query
query_lines = [
"TABLE file.mtime",
f"WHERE file.mtime >= date(today) - dur({days} days)",
"SORT file.mtime DESC",
f"LIMIT {limit}"
]
# Join with proper DQL line breaks
dql_query = "\n".join(query_lines)
# Make the request to search endpoint
url = f"{self.get_base_url()}/search/"
headers = self._get_headers() | {
'Content-Type': 'application/vnd.olrapi.dataview.dql+txt'
}
def call_fn():
response = requests.post(
url,
headers=headers,
data=dql_query.encode('utf-8'),
verify=self.verify_ssl,
timeout=self.timeout
)
response.raise_for_status()
return response.json()
return self._safe_call(call_fn)

View File

@@ -1,4 +1,3 @@
import json
import logging
from collections.abc import Sequence
@@ -6,31 +5,37 @@ from functools import lru_cache
from typing import Any
import os
from dotenv import load_dotenv
from mcp.server import Server
from mcp.server import Server as MCPServer
from contextlib import asynccontextmanager
from mcp.types import (
Resource,
Tool,
TextContent,
ImageContent,
EmbeddedResource,
LoggingLevel,
)
load_dotenv()
from . import obsidian
from . import tools
# Load environment variables
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("mcp-knowledge-base")
logger = logging.getLogger("mcp-obsidian")
api_key = os.getenv("OBSIDIAN_API_KEY")
if not api_key:
raise ValueError("OBSIDIAN_API_KEY environment variable required")
@asynccontextmanager
async def lifespan(app: MCPServer):
api_key = os.getenv("OBSIDIAN_API_KEY")
if not api_key:
raise ValueError(f"OBSIDSIAN_API_KEY environment variable required. Working directory: {os.getcwd()}")
yield
app = Server("mcp-knowledge-base")
app = MCPServer(
"mcp-obsidian",
lifespan=lifespan,
)
tool_handlers = {}
def add_tool_handler(tool_class: tools.ToolHandler):
@@ -50,17 +55,13 @@ add_tool_handler(tools.GetFileContentsToolHandler())
add_tool_handler(tools.SearchToolHandler())
add_tool_handler(tools.PatchContentToolHandler())
add_tool_handler(tools.AppendContentToolHandler())
add_tool_handler(tools.PutContentToolHandler())
add_tool_handler(tools.DeleteFileToolHandler())
add_tool_handler(tools.ComplexSearchToolHandler())
#@app.list_resources()
#async def list_resources() -> list[Resource]:
# return [
# Resource(
# uri="obisidian:///note/app.log",
# name="Application Logs",
# mimeType="text/plain"
# )
# ]
add_tool_handler(tools.BatchGetFileContentsToolHandler())
add_tool_handler(tools.PeriodicNotesToolHandler())
add_tool_handler(tools.RecentPeriodicNotesToolHandler())
add_tool_handler(tools.RecentChangesToolHandler())
@app.list_tools()
async def list_tools() -> list[Tool]:
@@ -69,7 +70,7 @@ async def list_tools() -> list[Tool]:
return [th.get_tool_description() for th in tool_handlers.values()]
@app.call_tool()
async def call_tool(name: str, arguments: Any) -> Sequence[TextContent]:
async def call_tool(name: str, arguments: Any) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
"""Handle tool calls for command line run."""
if not isinstance(arguments, dict):
@@ -98,6 +99,3 @@ async def main():
write_stream,
app.create_initialization_options()
)

660
src/mcp_obsidian/tools.py Normal file
View File

@@ -0,0 +1,660 @@
from collections.abc import Sequence
from mcp.types import (
Tool,
TextContent,
ImageContent,
EmbeddedResource,
)
import json
import os
from . import obsidian
def create_obsidian_api() -> obsidian.Obsidian:
"""Factory function to create Obsidian API instances.
Creates a new Obsidian API instance with parsed configuration from environment variables.
This centralizes the configuration logic and makes testing easier.
Returns:
Configured Obsidian API instance
Raises:
Exception: If configuration is invalid or instance creation fails
"""
# Load environment variables
api_key = os.getenv("OBSIDIAN_API_KEY", "")
obsidian_host = os.getenv("OBSIDIAN_HOST", "https://127.0.0.1:27124")
if api_key == "":
raise ValueError(f"OBSIDIAN_API_KEY environment variable required. Working directory: {os.getcwd()}")
# Parse the OBSIDIAN_HOST configuration at module level for validation
try:
protocol, host, port, path = obsidian.Obsidian.parse_host_config(obsidian_host)
except ValueError as e:
raise ValueError(f"Invalid OBSIDIAN_HOST configuration: {str(e)}")
try:
return obsidian.Obsidian(
api_key=api_key,
protocol=protocol,
host=host,
port=port,
path=path,
verify_ssl=False # Default to False for local development
)
except Exception as e:
raise Exception(f"Failed to create Obsidian API instance: {str(e)}")
TOOL_LIST_FILES_IN_VAULT = "obsidian_list_files_in_vault"
TOOL_LIST_FILES_IN_DIR = "obsidian_list_files_in_dir"
class ToolHandler():
def __init__(self, tool_name: str):
self.name = tool_name
def get_tool_description(self) -> Tool:
raise NotImplementedError()
def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
raise NotImplementedError()
class ListFilesInVaultToolHandler(ToolHandler):
def __init__(self):
super().__init__(TOOL_LIST_FILES_IN_VAULT)
def get_tool_description(self):
return Tool(
name=self.name,
description="Lists all files and directories in the root directory of your Obsidian vault.",
inputSchema={
"type": "object",
"properties": {},
"required": []
},
)
def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
api = create_obsidian_api()
files = api.list_files_in_vault()
return [
TextContent(
type="text",
text=json.dumps(files, indent=2)
)
]
class ListFilesInDirToolHandler(ToolHandler):
def __init__(self):
super().__init__(TOOL_LIST_FILES_IN_DIR)
def get_tool_description(self):
return Tool(
name=self.name,
description="Lists all files and directories that exist in a specific Obsidian directory.",
inputSchema={
"type": "object",
"properties": {
"dirpath": {
"type": "string",
"description": "Path to list files from (relative to your vault root). Note that empty directories will not be returned."
},
},
"required": ["dirpath"]
}
)
def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
if "dirpath" not in args:
raise RuntimeError("dirpath argument missing in arguments")
api = create_obsidian_api()
files = api.list_files_in_dir(args["dirpath"])
return [
TextContent(
type="text",
text=json.dumps(files, indent=2)
)
]
class GetFileContentsToolHandler(ToolHandler):
def __init__(self):
super().__init__("obsidian_get_file_contents")
def get_tool_description(self):
return Tool(
name=self.name,
description="Return the content of a single file in your vault.",
inputSchema={
"type": "object",
"properties": {
"filepath": {
"type": "string",
"description": "Path to the relevant file (relative to your vault root).",
"format": "path"
},
},
"required": ["filepath"]
}
)
def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
if "filepath" not in args:
raise RuntimeError("filepath argument missing in arguments")
api = create_obsidian_api()
content = api.get_file_contents(args["filepath"])
return [
TextContent(
type="text",
text=json.dumps(content, indent=2)
)
]
class SearchToolHandler(ToolHandler):
def __init__(self):
super().__init__("obsidian_simple_search")
def get_tool_description(self):
return Tool(
name=self.name,
description="""Simple search for documents matching a specified text query across all files in the vault.
Use this tool when you want to do a simple text search""",
inputSchema={
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Text to a simple search for in the vault."
},
"context_length": {
"type": "integer",
"description": "How much context to return around the matching string (default: 100)",
"default": 100
}
},
"required": ["query"]
}
)
def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
if "query" not in args:
raise RuntimeError("query argument missing in arguments")
context_length = args.get("context_length", 100)
api = create_obsidian_api()
results = api.search(args["query"], context_length)
formatted_results = []
for result in results:
formatted_matches = []
for match in result.get('matches', []):
context = match.get('context', '')
match_pos = match.get('match', {})
start = match_pos.get('start', 0)
end = match_pos.get('end', 0)
formatted_matches.append({
'context': context,
'match_position': {'start': start, 'end': end}
})
formatted_results.append({
'filename': result.get('filename', ''),
'score': result.get('score', 0),
'matches': formatted_matches
})
return [
TextContent(
type="text",
text=json.dumps(formatted_results, indent=2)
)
]
class AppendContentToolHandler(ToolHandler):
def __init__(self):
super().__init__("obsidian_append_content")
def get_tool_description(self):
return Tool(
name=self.name,
description="Append content to a new or existing file in the vault.",
inputSchema={
"type": "object",
"properties": {
"filepath": {
"type": "string",
"description": "Path to the file (relative to vault root)",
"format": "path"
},
"content": {
"type": "string",
"description": "Content to append to the file"
}
},
"required": ["filepath", "content"]
}
)
def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
if "filepath" not in args or "content" not in args:
raise RuntimeError("filepath and content arguments required")
api = create_obsidian_api()
api.append_content(args.get("filepath", ""), args["content"])
return [
TextContent(
type="text",
text=f"Successfully appended content to {args['filepath']}"
)
]
class PatchContentToolHandler(ToolHandler):
def __init__(self):
super().__init__("obsidian_patch_content")
def get_tool_description(self):
return Tool(
name=self.name,
description="Insert content into an existing note relative to a heading, block reference, or frontmatter field.",
inputSchema={
"type": "object",
"properties": {
"filepath": {
"type": "string",
"description": "Path to the file (relative to vault root)",
"format": "path"
},
"operation": {
"type": "string",
"description": "Operation to perform (append, prepend, or replace)",
"enum": ["append", "prepend", "replace"]
},
"target_type": {
"type": "string",
"description": "Type of target to patch",
"enum": ["heading", "block", "frontmatter"]
},
"target": {
"type": "string",
"description": "Target identifier (heading path, block reference, or frontmatter field)"
},
"content": {
"type": "string",
"description": "Content to insert"
}
},
"required": ["filepath", "operation", "target_type", "target", "content"]
}
)
def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
if not all(k in args for k in ["filepath", "operation", "target_type", "target", "content"]):
raise RuntimeError("filepath, operation, target_type, target and content arguments required")
api = create_obsidian_api()
api.patch_content(
args.get("filepath", ""),
args.get("operation", ""),
args.get("target_type", ""),
args.get("target", ""),
args.get("content", "")
)
return [
TextContent(
type="text",
text=f"Successfully patched content in {args['filepath']}"
)
]
class PutContentToolHandler(ToolHandler):
def __init__(self):
super().__init__("obsidian_put_content")
def get_tool_description(self):
return Tool(
name=self.name,
description="Create a new file in your vault or update the content of an existing one in your vault.",
inputSchema={
"type": "object",
"properties": {
"filepath": {
"type": "string",
"description": "Path to the relevant file (relative to your vault root)",
"format": "path"
},
"content": {
"type": "string",
"description": "Content of the file you would like to upload"
}
},
"required": ["filepath", "content"]
}
)
def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
if "filepath" not in args or "content" not in args:
raise RuntimeError("filepath and content arguments required")
api = create_obsidian_api()
api.put_content(args.get("filepath", ""), args["content"])
return [
TextContent(
type="text",
text=f"Successfully uploaded content to {args['filepath']}"
)
]
class DeleteFileToolHandler(ToolHandler):
def __init__(self):
super().__init__("obsidian_delete_file")
def get_tool_description(self):
return Tool(
name=self.name,
description="Delete a file or directory from the vault.",
inputSchema={
"type": "object",
"properties": {
"filepath": {
"type": "string",
"description": "Path to the file or directory to delete (relative to vault root)",
"format": "path"
},
"confirm": {
"type": "boolean",
"description": "Confirmation to delete the file (must be true)",
"default": False
}
},
"required": ["filepath", "confirm"]
}
)
def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
if "filepath" not in args:
raise RuntimeError("filepath argument missing in arguments")
if not args.get("confirm", False):
raise RuntimeError("confirm must be set to true to delete a file")
api = create_obsidian_api()
api.delete_file(args["filepath"])
return [
TextContent(
type="text",
text=f"Successfully deleted {args['filepath']}"
)
]
class ComplexSearchToolHandler(ToolHandler):
def __init__(self):
super().__init__("obsidian_complex_search")
def get_tool_description(self):
return Tool(
name=self.name,
description="""Complex search for documents using a JsonLogic query.
Supports standard JsonLogic operators plus 'glob' and 'regexp' for pattern matching. Results must be non-falsy.
Use this tool when you want to do a complex search, e.g. for all documents with certain tags etc.
ALWAYS follow query syntax in examples.
Examples
1. Match all markdown files
{"glob": ["*.md", {"var": "path"}]}
2. Match all markdown files with 1221 substring inside them
{
"and": [
{ "glob": ["*.md", {"var": "path"}] },
{ "regexp": [".*1221.*", {"var": "content"}] }
]
}
3. Match all markdown files in Work folder containing name Keaton
{
"and": [
{ "glob": ["*.md", {"var": "path"}] },
{ "regexp": [".*Work.*", {"var": "path"}] },
{ "regexp": ["Keaton", {"var": "content"}] }
]
}
""",
inputSchema={
"type": "object",
"properties": {
"query": {
"type": "object",
"description": "JsonLogic query object. ALWAYS follow query syntax in examples. \
Example 1: {\"glob\": [\"*.md\", {\"var\": \"path\"}]} matches all markdown files \
Example 2: {\"and\": [{\"glob\": [\"*.md\", {\"var\": \"path\"}]}, {\"regexp\": [\".*1221.*\", {\"var\": \"content\"}]}]} matches all markdown files with 1221 substring inside them \
Example 3: {\"and\": [{\"glob\": [\"*.md\", {\"var\": \"path\"}]}, {\"regexp\": [\".*Work.*\", {\"var\": \"path\"}]}, {\"regexp\": [\"Keaton\", {\"var\": \"content\"}]}]} matches all markdown files in Work folder containing name Keaton \
"
}
},
"required": ["query"]
}
)
def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
if "query" not in args:
raise RuntimeError("query argument missing in arguments")
api = create_obsidian_api()
results = api.search_json(args.get("query", ""))
return [
TextContent(
type="text",
text=json.dumps(results, indent=2)
)
]
class BatchGetFileContentsToolHandler(ToolHandler):
def __init__(self):
super().__init__("obsidian_batch_get_file_contents")
def get_tool_description(self):
return Tool(
name=self.name,
description="Return the contents of multiple files in your vault, concatenated with headers.",
inputSchema={
"type": "object",
"properties": {
"filepaths": {
"type": "array",
"items": {
"type": "string",
"description": "Path to a file (relative to your vault root)",
"format": "path"
},
"description": "List of file paths to read"
},
},
"required": ["filepaths"]
}
)
def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
if "filepaths" not in args:
raise RuntimeError("filepaths argument missing in arguments")
api = create_obsidian_api()
content = api.get_batch_file_contents(args["filepaths"])
return [
TextContent(
type="text",
text=content
)
]
class PeriodicNotesToolHandler(ToolHandler):
def __init__(self):
super().__init__("obsidian_get_periodic_note")
def get_tool_description(self):
return Tool(
name=self.name,
description="Get current periodic note for the specified period.",
inputSchema={
"type": "object",
"properties": {
"period": {
"type": "string",
"description": "The period type (daily, weekly, monthly, quarterly, yearly)",
"enum": ["daily", "weekly", "monthly", "quarterly", "yearly"]
},
"type": {
"type": "string",
"description": "The type of data to get ('content' or 'metadata'). 'content' returns just the content in Markdown format. 'metadata' includes note metadata (including paths, tags, etc.) and the content.",
"default": "content",
"enum": ["content", "metadata"]
}
},
"required": ["period"]
}
)
def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
if "period" not in args:
raise RuntimeError("period argument missing in arguments")
period = args["period"]
valid_periods = ["daily", "weekly", "monthly", "quarterly", "yearly"]
if period not in valid_periods:
raise RuntimeError(f"Invalid period: {period}. Must be one of: {', '.join(valid_periods)}")
type = args["type"] if "type" in args else "content"
valid_types = ["content", "metadata"]
if type not in valid_types:
raise RuntimeError(f"Invalid type: {type}. Must be one of: {', '.join(valid_types)}")
api = create_obsidian_api()
content = api.get_periodic_note(period)
return [
TextContent(
type="text",
text=content
)
]
class RecentPeriodicNotesToolHandler(ToolHandler):
def __init__(self):
super().__init__("obsidian_get_recent_periodic_notes")
def get_tool_description(self):
return Tool(
name=self.name,
description="Get most recent periodic notes for the specified period type.",
inputSchema={
"type": "object",
"properties": {
"period": {
"type": "string",
"description": "The period type (daily, weekly, monthly, quarterly, yearly)",
"enum": ["daily", "weekly", "monthly", "quarterly", "yearly"]
},
"limit": {
"type": "integer",
"description": "Maximum number of notes to return (default: 5)",
"default": 5,
"minimum": 1,
"maximum": 50
},
"include_content": {
"type": "boolean",
"description": "Whether to include note content (default: false)",
"default": False
}
},
"required": ["period"]
}
)
def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
if "period" not in args:
raise RuntimeError("period argument missing in arguments")
period = args["period"]
valid_periods = ["daily", "weekly", "monthly", "quarterly", "yearly"]
if period not in valid_periods:
raise RuntimeError(f"Invalid period: {period}. Must be one of: {', '.join(valid_periods)}")
limit = args.get("limit", 5)
if not isinstance(limit, int) or limit < 1:
raise RuntimeError(f"Invalid limit: {limit}. Must be a positive integer")
include_content = args.get("include_content", False)
if not isinstance(include_content, bool):
raise RuntimeError(f"Invalid include_content: {include_content}. Must be a boolean")
api = create_obsidian_api()
results = api.get_recent_periodic_notes(period, limit, include_content)
return [
TextContent(
type="text",
text=json.dumps(results, indent=2)
)
]
class RecentChangesToolHandler(ToolHandler):
def __init__(self):
super().__init__("obsidian_get_recent_changes")
def get_tool_description(self):
return Tool(
name=self.name,
description="Get recently modified files in the vault.",
inputSchema={
"type": "object",
"properties": {
"limit": {
"type": "integer",
"description": "Maximum number of files to return (default: 10)",
"default": 10,
"minimum": 1,
"maximum": 100
},
"days": {
"type": "integer",
"description": "Only include files modified within this many days (default: 90)",
"minimum": 1,
"default": 90
}
}
}
)
def run_tool(self, args: dict) -> Sequence[TextContent | ImageContent | EmbeddedResource]:
limit = args.get("limit", 10)
if not isinstance(limit, int) or limit < 1:
raise RuntimeError(f"Invalid limit: {limit}. Must be a positive integer")
days = args.get("days", 90)
if not isinstance(days, int) or days < 1:
raise RuntimeError(f"Invalid days: {days}. Must be a positive integer")
api = create_obsidian_api()
results = api.get_recent_changes(limit, days)
return [
TextContent(
type="text",
text=json.dumps(results, indent=2)
)
]

40
tests/integration_test.py Normal file
View File

@@ -0,0 +1,40 @@
import os
import sys
import unittest
# Add the src directory to the Python path
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src')))
from mcp_obsidian import obsidian
class TestObsidianIntegration(unittest.TestCase):
def setUp(self):
"""Set up the environment variables for the test."""
os.environ['OBSIDIAN_API_KEY'] = 'REDACTED_API_KEY'
os.environ['OBSIDIAN_HOST'] = 'http://obsidian.obsidian.svc.cluster.local:27123'
def test_connection(self):
"""Test the connection to the Obsidian API."""
try:
protocol, host, port, path = obsidian.Obsidian.parse_host_config(os.environ['OBSIDIAN_HOST'])
api = obsidian.Obsidian(
api_key=os.environ['OBSIDIAN_API_KEY'],
protocol=protocol,
host=host,
port=port,
path=path,
verify_ssl=False
)
# Use a basic API call to verify the connection
files = api.list_files_in_vault()
self.assertIsNotNone(files)
print("Successfully connected to Obsidian API and listed files.")
except Exception as e:
self.fail(f"Failed to connect to Obsidian API: {e}")
if __name__ == '__main__':
unittest.main()

117
uv.lock generated
View File

@@ -1,5 +1,5 @@
version = 1
requires-python = ">=3.13"
requires-python = ">=3.11"
[[package]]
name = "annotated-types"
@@ -38,6 +38,36 @@ version = "3.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 },
{ url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 },
{ url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 },
{ url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 },
{ url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 },
{ url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 },
{ url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 },
{ url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 },
{ url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 },
{ url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 },
{ url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 },
{ url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 },
{ url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 },
{ url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 },
{ url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 },
{ url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445 },
{ url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275 },
{ url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020 },
{ url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128 },
{ url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277 },
{ url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174 },
{ url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838 },
{ url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149 },
{ url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043 },
{ url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229 },
{ url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556 },
{ url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772 },
{ url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 },
{ url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 },
{ url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 },
{ url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 },
{ url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 },
{ url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 },
@@ -61,7 +91,7 @@ name = "click"
version = "8.1.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "platform_system == 'Windows'" },
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 }
wheels = [
@@ -101,18 +131,17 @@ wheels = [
[[package]]
name = "httpx"
version = "0.27.2"
version = "0.28.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
{ name = "sniffio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189 }
sdist = { url = "https://files.pythonhosted.org/packages/10/df/676b7cf674dd1bdc71a64ad393c89879f75e4a0ab8395165b498262ae106/httpx-0.28.0.tar.gz", hash = "sha256:0858d3bab51ba7e386637f22a61d8ccddaeec5f3fe4209da3a6168dbb91573e0", size = 141307 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395 },
{ url = "https://files.pythonhosted.org/packages/8f/fb/a19866137577ba60c6d8b69498dc36be479b13ba454f691348ddf428f185/httpx-0.28.0-py3-none-any.whl", hash = "sha256:dc0b419a0cfeb6e8b34e85167c0da2671206f5095f1baa9663d23bcfd6b535fc", size = 73551 },
]
[[package]]
@@ -135,7 +164,7 @@ wheels = [
[[package]]
name = "mcp"
version = "1.0.0"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -145,14 +174,14 @@ dependencies = [
{ name = "sse-starlette" },
{ name = "starlette" },
]
sdist = { url = "https://files.pythonhosted.org/packages/97/de/a9ec0a1b6439f90ea59f89004bb2e7ec6890dfaeef809751d9e6577dca7e/mcp-1.0.0.tar.gz", hash = "sha256:dba51ce0b5c6a80e25576f606760c49a91ee90210fed805b530ca165d3bbc9b7", size = 82891 }
sdist = { url = "https://files.pythonhosted.org/packages/77/f2/067b1fc114e8d3ae4af02fc4f4ed8971a2c4900362d976fabe0f4e9a3418/mcp-1.1.0.tar.gz", hash = "sha256:e3c8d6df93a4de90230ea944dd667730744a3cd91a4cc0ee66a5acd53419e100", size = 83802 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/56/89/900c0c8445ec001d3725e475fc553b0feb2e8a51be018f3bb7de51e683db/mcp-1.0.0-py3-none-any.whl", hash = "sha256:bbe70ffa3341cd4da78b5eb504958355c68381fb29971471cea1e642a2af5b8a", size = 36361 },
{ url = "https://files.pythonhosted.org/packages/b9/3e/aef19ac08a6f9a347c086c4e628c2f7329659828cbe92ffd524ec2aac833/mcp-1.1.0-py3-none-any.whl", hash = "sha256:44aa4d2e541f0924d6c344aa7f96b427a6ee1df2fab70b5f9ae2f8777b3f05f2", size = 36576 },
]
[[package]]
name = "mcp-knowledge-base"
version = "0.1.0"
name = "mcp-obsidian"
version = "0.2.1"
source = { editable = "." }
dependencies = [
{ name = "mcp" },
@@ -160,25 +189,42 @@ dependencies = [
{ name = "requests" },
]
[package.dev-dependencies]
dev = [
{ name = "pyright" },
]
[package.metadata]
requires-dist = [
{ name = "mcp", specifier = ">=1.0.0" },
{ name = "mcp", specifier = ">=1.1.0" },
{ name = "python-dotenv", specifier = ">=1.0.1" },
{ name = "requests", specifier = ">=2.32.3" },
]
[package.metadata.requires-dev]
dev = [{ name = "pyright", specifier = ">=1.1.389" }]
[[package]]
name = "nodeenv"
version = "1.9.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 },
]
[[package]]
name = "pydantic"
version = "2.10.2"
version = "2.10.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/41/86/a03390cb12cf64e2a8df07c267f3eb8d5035e0f9a04bb20fb79403d2a00e/pydantic-2.10.2.tar.gz", hash = "sha256:2bc2d7f17232e0841cbba4641e65ba1eb6fafb3a08de3a091ff3ce14a197c4fa", size = 785401 }
sdist = { url = "https://files.pythonhosted.org/packages/45/0f/27908242621b14e649a84e62b133de45f84c255eecb350ab02979844a788/pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9", size = 786486 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d5/74/da832196702d0c56eb86b75bfa346db9238617e29b0b7ee3b8b4eccfe654/pydantic-2.10.2-py3-none-any.whl", hash = "sha256:cfb96e45951117c3024e6b67b25cdc33a3cb7b2fa62e239f7af1378358a1d99e", size = 456364 },
{ url = "https://files.pythonhosted.org/packages/62/51/72c18c55cf2f46ff4f91ebcc8f75aa30f7305f3d726be3f4ebffb4ae972b/pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d", size = 456997 },
]
[[package]]
@@ -190,6 +236,34 @@ dependencies = [
]
sdist = { url = "https://files.pythonhosted.org/packages/a6/9f/7de1f19b6aea45aeb441838782d68352e71bfa98ee6fa048d5041991b33e/pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235", size = 412785 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/27/39/46fe47f2ad4746b478ba89c561cafe4428e02b3573df882334bd2964f9cb/pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8", size = 1895553 },
{ url = "https://files.pythonhosted.org/packages/1c/00/0804e84a78b7fdb394fff4c4f429815a10e5e0993e6ae0e0b27dd20379ee/pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330", size = 1807220 },
{ url = "https://files.pythonhosted.org/packages/01/de/df51b3bac9820d38371f5a261020f505025df732ce566c2a2e7970b84c8c/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52", size = 1829727 },
{ url = "https://files.pythonhosted.org/packages/5f/d9/c01d19da8f9e9fbdb2bf99f8358d145a312590374d0dc9dd8dbe484a9cde/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4", size = 1854282 },
{ url = "https://files.pythonhosted.org/packages/5f/84/7db66eb12a0dc88c006abd6f3cbbf4232d26adfd827a28638c540d8f871d/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c", size = 2037437 },
{ url = "https://files.pythonhosted.org/packages/34/ac/a2537958db8299fbabed81167d58cc1506049dba4163433524e06a7d9f4c/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de", size = 2780899 },
{ url = "https://files.pythonhosted.org/packages/4a/c1/3e38cd777ef832c4fdce11d204592e135ddeedb6c6f525478a53d1c7d3e5/pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025", size = 2135022 },
{ url = "https://files.pythonhosted.org/packages/7a/69/b9952829f80fd555fe04340539d90e000a146f2a003d3fcd1e7077c06c71/pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e", size = 1987969 },
{ url = "https://files.pythonhosted.org/packages/05/72/257b5824d7988af43460c4e22b63932ed651fe98804cc2793068de7ec554/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919", size = 1994625 },
{ url = "https://files.pythonhosted.org/packages/73/c3/78ed6b7f3278a36589bcdd01243189ade7fc9b26852844938b4d7693895b/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c", size = 2090089 },
{ url = "https://files.pythonhosted.org/packages/8d/c8/b4139b2f78579960353c4cd987e035108c93a78371bb19ba0dc1ac3b3220/pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc", size = 2142496 },
{ url = "https://files.pythonhosted.org/packages/3e/f8/171a03e97eb36c0b51981efe0f78460554a1d8311773d3d30e20c005164e/pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9", size = 1811758 },
{ url = "https://files.pythonhosted.org/packages/6a/fe/4e0e63c418c1c76e33974a05266e5633e879d4061f9533b1706a86f77d5b/pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5", size = 1980864 },
{ url = "https://files.pythonhosted.org/packages/50/fc/93f7238a514c155a8ec02fc7ac6376177d449848115e4519b853820436c5/pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89", size = 1864327 },
{ url = "https://files.pythonhosted.org/packages/be/51/2e9b3788feb2aebff2aa9dfbf060ec739b38c05c46847601134cc1fed2ea/pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f", size = 1895239 },
{ url = "https://files.pythonhosted.org/packages/7b/9e/f8063952e4a7d0127f5d1181addef9377505dcce3be224263b25c4f0bfd9/pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02", size = 1805070 },
{ url = "https://files.pythonhosted.org/packages/2c/9d/e1d6c4561d262b52e41b17a7ef8301e2ba80b61e32e94520271029feb5d8/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c", size = 1828096 },
{ url = "https://files.pythonhosted.org/packages/be/65/80ff46de4266560baa4332ae3181fffc4488ea7d37282da1a62d10ab89a4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac", size = 1857708 },
{ url = "https://files.pythonhosted.org/packages/d5/ca/3370074ad758b04d9562b12ecdb088597f4d9d13893a48a583fb47682cdf/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb", size = 2037751 },
{ url = "https://files.pythonhosted.org/packages/b1/e2/4ab72d93367194317b99d051947c071aef6e3eb95f7553eaa4208ecf9ba4/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529", size = 2733863 },
{ url = "https://files.pythonhosted.org/packages/8a/c6/8ae0831bf77f356bb73127ce5a95fe115b10f820ea480abbd72d3cc7ccf3/pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35", size = 2161161 },
{ url = "https://files.pythonhosted.org/packages/f1/f4/b2fe73241da2429400fc27ddeaa43e35562f96cf5b67499b2de52b528cad/pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089", size = 1993294 },
{ url = "https://files.pythonhosted.org/packages/77/29/4bb008823a7f4cc05828198153f9753b3bd4c104d93b8e0b1bfe4e187540/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381", size = 2001468 },
{ url = "https://files.pythonhosted.org/packages/f2/a9/0eaceeba41b9fad851a4107e0cf999a34ae8f0d0d1f829e2574f3d8897b0/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb", size = 2091413 },
{ url = "https://files.pythonhosted.org/packages/d8/36/eb8697729725bc610fd73940f0d860d791dc2ad557faaefcbb3edbd2b349/pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae", size = 2154735 },
{ url = "https://files.pythonhosted.org/packages/52/e5/4f0fbd5c5995cc70d3afed1b5c754055bb67908f55b5cb8000f7112749bf/pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c", size = 1833633 },
{ url = "https://files.pythonhosted.org/packages/ee/f2/c61486eee27cae5ac781305658779b4a6b45f9cc9d02c90cb21b940e82cc/pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16", size = 1986973 },
{ url = "https://files.pythonhosted.org/packages/df/a6/e3f12ff25f250b02f7c51be89a294689d175ac76e1096c32bf278f29ca1e/pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e", size = 1883215 },
{ url = "https://files.pythonhosted.org/packages/0f/d6/91cb99a3c59d7b072bded9959fbeab0a9613d5a4935773c0801f1764c156/pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073", size = 1895033 },
{ url = "https://files.pythonhosted.org/packages/07/42/d35033f81a28b27dedcade9e967e8a40981a765795c9ebae2045bcef05d3/pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08", size = 1807542 },
{ url = "https://files.pythonhosted.org/packages/41/c2/491b59e222ec7e72236e512108ecad532c7f4391a14e971c963f624f7569/pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf", size = 1827854 },
@@ -206,6 +280,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/df/c3/b15fb833926d91d982fde29c0624c9f225da743c7af801dace0d4e187e71/pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5", size = 1882983 },
]
[[package]]
name = "pyright"
version = "1.1.389"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nodeenv" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/72/4e/9a5ab8745e7606b88c2c7ca223449ac9d82a71fd5e31df47b453f2cb39a1/pyright-1.1.389.tar.gz", hash = "sha256:716bf8cc174ab8b4dcf6828c3298cac05c5ed775dda9910106a5dcfe4c7fe220", size = 21940 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1b/26/c288cabf8cfc5a27e1aa9e5029b7682c0f920b8074f45d22bf844314d66a/pyright-1.1.389-py3-none-any.whl", hash = "sha256:41e9620bba9254406dc1f621a88ceab5a88af4c826feb4f614d95691ed243a60", size = 18581 },
]
[[package]]
name = "python-dotenv"
version = "1.0.1"