Compare commits
52 Commits
9a7b5b6877
...
e5e1c1e11c
Author | SHA1 | Date | |
---|---|---|---|
e5e1c1e11c | |||
cf48f23e8e | |||
0b746f65e9 | |||
897822ecaa | |||
00245ef40b | |||
8bca179ba5 | |||
|
72490a4db0 | ||
|
24cb2b9cbd | ||
|
741c554935 | ||
|
0903b9bcc8 | ||
|
63706f0968 | ||
|
cefbd684bf | ||
|
8501758bfd | ||
|
f1421fe292 | ||
|
168320f01b | ||
|
64e28409cc | ||
|
9df618eb3d | ||
|
7a8b723485 | ||
|
5cf106e30d | ||
|
44c8d1540f | ||
|
6de8d8cd28 | ||
|
51398322bf | ||
|
f3a802da70 | ||
|
0f55b50aaa | ||
|
3f22521b01 | ||
|
a86a6de1f4 | ||
|
22177e44c9 | ||
|
182b42b567 | ||
|
dd7dfb56c4 | ||
|
4c9cea7f30 | ||
|
e6ef6a1e6d | ||
|
c813c12a06 | ||
|
c1ae1eeec7 | ||
|
17a0e5b2b7 | ||
|
07ced693ce | ||
|
971c1edd34 | ||
|
0041778e4f | ||
|
0b65b77370 | ||
|
2b7edd8283 | ||
|
8494bdf83b | ||
|
aa56549dc2 | ||
|
1b75b7db07 | ||
|
c43fc39156 | ||
|
abdc8fd875 | ||
|
b88fc57b5d | ||
|
b1f68a4949 | ||
|
07e3697f27 | ||
|
6d671ea0ff | ||
|
970a9b061e | ||
|
12ea3bc43c | ||
|
9e9547e45d | ||
|
dd58a39d03 |
21
LICENSE
Normal file
21
LICENSE
Normal 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.
|
97
README.md
97
README.md
@@ -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
|
||||
```
|
||||
|
507
openapi.yaml
507
openapi.yaml
@@ -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).
|
||||
|
@@ -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"
|
||||
|
@@ -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)
|
@@ -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)
|
||||
)
|
||||
]
|
374
src/mcp_obsidian/obsidian.py
Normal file
374
src/mcp_obsidian/obsidian.py
Normal 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)
|
@@ -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
660
src/mcp_obsidian/tools.py
Normal 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
40
tests/integration_test.py
Normal 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
117
uv.lock
generated
@@ -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"
|
||||
|
Reference in New Issue
Block a user